How to use React Streaming In Remix

How to use React Streaming In Remix

React 18 is the latest version of the popular JavaScript library for building user interfaces, and it comes with various new features and improvements, including streaming server-side rendering. Remix, on the other hand, is a framework built on top of React that provides a better developer experience for building and shipping web applications.

In this blog post, we will discuss how to use React 18 streaming features in Remix and the benefits of doing so. You need some knowledge of React and Remix if you want to understand some specific terminologies used in this piece.

Benefits of React 18 Streaming in Remix

Using React 18 streaming in Remix has several benefits. Firstly, it can lead to faster page loads and a better user experience. Instead of waiting for the entire page to render, the browser can start rendering parts of it as soon as the server sends them. This means users can start interacting with the page faster, leading to a better user experience. Secondly, it can help reduce server load and improve scalability. With streaming server-side rendering, the server can start delivering content as soon as it becomes available, which means it can handle more requests without overloading.

Finally, it can make it easier to build applications that require real-time updates, such as chat applications or stock tickers. With streaming server-side rendering, changes to the page can be delivered to users in real-time, making it easier to build these types of applications.

The Problem

Let's take a look at the code for a page that loads without streaming.

export const loader = async ({ params }: LoaderArgs) => {
  const id = params.id;
  if (!id) throw new Error("ID is required");

  return json(await getMovie(id));
};

The getMovie() function is defined in a different module as follows:

export async function getMovie(id: string) {
  const movieResponse = await fetch(
    `https://api.themoviedb.org/3/movie/${id}?api_key=${API_KEY}`
  );

  const similarMovies = await fetch(
    `https://api.themoviedb.org/3/movie/${id}/similar?api_key=${API_KEY}`
  );

  const credits = await fetch(
    `https://api.themoviedb.org/3/movie/${id}/credits?api_key=${API_KEY}`
  );

  const { cast, crew } = await credits.json<MovieCredits>();
  const movie = await movieResponse.json<Movie>()

  return {
    movie,
    cast,
    crew,
    similar: (await similarMovies.json<{ results: Movie[] }>()).results,
  };
}

In the code above, we're fetching the movie info, then the credits, and finally, a list of related movies. All the data must be loaded before the page is served to the user. If loading related movies takes about two seconds to load, then the whole page will take as long as this slow data.

What if we can start rendering the page with as little data as possible and display the rest when they're ready? We can do that with React streaming in Remix.

Using React 18 Streaming in Remix

First, to enable streaming with React 18, you'll update your entry.server.tsx file to use renderToPipeableStream or renderToReadableStream, depending on if you’re running in Node.js or Web Streams API

Let’s imagine the server is running on Cloudflare Workers, therefore I’ll use renderToReadableStream like this:

import type { EntryContext } from "@remix-run/cloudflare"; // or node/deno
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToReadableStream } from "react-dom/server";

const ABORT_DELAY = 5000;

const handleRequest = async (
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) => {
  let didError = false;

  const stream = await renderToReadableStream(
    <RemixServer
      context={remixContext}
      url={request.url}
      abortDelay={ABORT_DELAY}
    />,
    {
      onError: (error: unknown) => {
        console.log("Caught an error");
        didError = true;
        console.error(error);

        // You can also log crash/error report
      },
      signal: AbortSignal.timeout(ABORT_DELAY),
    }
  );

  if (isbot(request.headers.get("user-agent"))) {
    await stream.allReady;
  }

  responseHeaders.set("Content-Type", "text/html");
  return new Response(stream, {
    headers: responseHeaders,
    status: didError ? 500 : responseStatusCode,
  });
};

export default handleRequest;

Then on the client, you need to make sure you're hydrating properly with React 18 hydrateRoot API:

//entry.client.tsx
hydrateRoot(document, <RemixBrowser />);

With that in place, you won’t see a significant performance improvement. But with that alone, you can now use [React.lazy](https://reactjs.org/docs/code-splitting.html#reactlazy) to SSR components but delay hydration on the client. This can open up network bandwidth for more critical things like styles, images, and fonts, leading to a better Largest Contentful Paint (LCP) and Time to Interactive (TTI).

Returning a deferred response

With React streaming set up, you can now start returning slow data requests using defer, then use <Await /> where you'd rather render a fallback UI. Let's do that for our example:

import { defer } from "@remix-run/cloudflare"; // or node/deno

export const loader = async ({ params }: LoaderArgs) => {
  const id = params.id;
  if (!id) throw new Error("ID is required");

  return defer(await getMovie(id));
};

The difference is that we replaced the return statement with the defer() function instead of json(). Next, let's update the getMovie() function to return similar movies as a promise, which will cause the UI to defer loading the related component until the data is ready.

export async function getMovie(id: string) {
  const movieResponse = await fetch(
    `https://api.themoviedb.org/3/movie/${id}?api_key=${API_KEY}`
  );

  const credits = await fetch(
    `https://api.themoviedb.org/3/movie/${id}/credits?api_key=${API_KEY}`
  );

  const similarMoviesPromise = fetch(
    `https://api.themoviedb.org/3/movie/${id}/similar?api_key=${API_KEY}`
  );

  const { cast, crew } = await credits.json<MovieCredits>();
  const movie = await movieResponse.json<Movie>()

  return {
    movie,
    cast,
    crew,
    similar: similarMoviesPromise
      .then((response) => response.json<{ results: Movie[] }>())
      .then((movies) => movies.results),
  };
}

The movie detail, casts, and crews are returned immediately, while similar movies are returned as a promise.

With this data, we can use React's <Suspense /> and Remix's <Await /> to render the data with a fallback UI:

<Suspense fallback={<p>Loading similar movies...</p>}>
  <Await
    resolve={similar}
    errorElement={<p>Error loading similar movies!</p>}
  >
    {(similar) => (
      <div className="p-12">
        <Category movies={similar} header={"More like this 🔥"} />
      </div>
    )}
  </Await>
</Suspense>

In the code above, the <Await /> component receives two props:

  1. resolve: This represents the data being streamed from the server

  2. errorElement: This represents the element that should be rendered if the server throws an error while streaming the data.

The page would then render as you see in the video below:

Conclusion

React 18 streaming is a powerful new feature that can improve the performance and scalability of web applications. It allows you to render parts of your UI to the client incrementally.

With Remix, it is easy to use this feature and build applications that provide a better user experience. Following the steps outlined in this blog post, you can start using React 18 streaming in your Remix application today.

I hope this blog post has been helpful in explaining how to use React 18 streaming in Remix and the benefits it provides. Happy coding 👩🏽‍💻!

References

Did you find this article valuable?

Support Peter's Blog by becoming a sponsor. Any amount is appreciated!