Upgrade your Remix app to React 18

Roger Stringer / July 05, 2022

3 min read2,694 views

It is easy to quickly upgrade a Remix app to React 18.

In this example, I'll use a fresh Remix installation by running:

npx create-remix@latest test

Then go into the folder and run:

npm install react@latest react-dom@latest isbot

Followed by:

npm install @types/react@latest @types/react-dom@latest --save-dev

This will upgrade our libraries to use React 18.

Now open your app/entry.client.tsx file:

app/entry.client.tsx
import { RemixBrowser } from "@remix-run/react";
import { hydrate } from "react-dom";

hydrate(<RemixBrowser />, document);

And replace with it a new version:

app/entry.client.tsx
import { hydrateRoot } from "react-dom/client";
import { RemixBrowser } from "@remix-run/react";

hydrateRoot(document, <RemixBrowser />);

Now let's do the same to our app/entry.server.tsx file, to use the new renderToPipeableStream API:

app/entry.server.tsx
import type { EntryContext } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  let markup = renderToString(
    <RemixServer context={remixContext} url={request.url} />
  );

  responseHeaders.set("Content-Type", "text/html");

  return new Response("<!DOCTYPE html>" + markup, {
    status: responseStatusCode,
    headers: responseHeaders,
  });
}

And replace with it a new version:

app/entry.server.tsx
import { PassThrough } from "stream";
import { renderToPipeableStream } from "react-dom/server";
import { RemixServer } from "@remix-run/react";
import type { EntryContext } from "@remix-run/node";
import { Response, Headers } from "@remix-run/node";
import isbot from "isbot";

const ABORT_DELAY = 5000;

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  const callbackName = isbot(request.headers.get("user-agent"))
    ? "onAllReady"
    : "onShellReady";

  return new Promise((resolve, reject) => {
    let didError = false;

    const { pipe, abort } = renderToPipeableStream(
      <RemixServer context={remixContext} url={request.url} />,
      {
        [callbackName]() {
          let body = new PassThrough();

          responseHeaders.set("Content-Type", "text/html");
          responseHeaders.set("Transfer-Encoding", "chunked");
          responseHeaders.set("Connection", "keep-alive");

          resolve(
            new Response(body, {
              status: didError ? 500 : responseStatusCode,
              headers: responseHeaders,
            })
          );
          pipe(body);
        },
        onShellError(err) {
          reject(err);
        },
        onError(error) {
          didError = true;
          console.error(error);
        },
      }
    );
    setTimeout(abort, ABORT_DELAY);
  });
}

Your Remix app now runs React 18 successfully. I've used this process to upgrade all of my Remix apps so far without any issues.

One Last Thing...

I bet you’re wondering why we do the isbot check right?

  const callbackName = isbot(request.headers.get("user-agent"))
    ? "onAllReady"
    : "onShellReady";

In React 18, onAllReady is used for static generation such as with crawlers, and onShellReady is called when all components are rendered before the suspense boundaries, if you decided to do all static then you can replace this with onAllReady instead.