DEV Community

Philipp Rich
Philipp Rich

Posted on • Originally published at philrich.dev

Migrating to React Router Framework: Challenges Beyond the Official Guide

My experience migrating from React Router library to the framework and the challenges missing from the official guide.

Motivation and tech stack

My small side project pixenum.com initially was a purely client SPA based on React and React Router(library mode).
I used Vite and Yarn Berry with PnP. For state management I used Redux.
This architecture comes with a cost of poor SEO. Traditional client side SPAs use an index file with minimum content in it, like this:

// index.html
...
    <body>
        <div id="root"></div>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Content then being loaded from .js scripts. But search engines crawl only a static content from .html, which is not present in case of a clien-side SPA.

To address this issue I aimed to divide my project into 2 parts:

  • / pre-rendered static(SSG) landing page for better SEO
  • /app actual app, which is a client-based(CSR) SPA

I saw several possible options, how to do this:

  • Create a separate package for the landing page on a static-site oriented framework, like Astro or React with a Vite plugin vite-react-ssg
  • Migrate to React Router Framework and specify / as a SSG route, while leave /app as a CSR route.

Starting point

Official migration docs: https://reactrouter.com/upgrading/component-routes

Instead of reiterating the migration guide, I'll focus on the specific challenges I encountered that were not covered.

PnP and RR Framework (isbot not found)

After following first part of migration guide i faced with dependency issue:

Cannot find module 'isbot' imported from 'MYPROJECT/.yarn/__virtual__/@react-router-dev-virtual-86a2f80477/4/.yarn/berry/cache/@react-router-dev-npm-7.2.0-2cf33a3d6a-10c0.zip/node_modules/@react-router/dev/dist/config/defaults/entry.server.node.tsx'
Enter fullscreen mode Exit fullscreen mode

It appeared that Yarn PnP was not including isbot due to the absence of an entry.server.tsx (which was not explicitely mentioned in migration guide).
To create default entry.server.tsx I called yarn run react-router reveal and this resolved problem with dependency.

SSL and RR Framework (":method" is an invalid header name)

After creating entry.server.tsx I got another issue:

[vite] Internal server error: Headers.set: ":method" is an invalid header name.
      at Object.webidl.errors.exception (node:internal/deps/undici/undici:3564:14)
      at Object.webidl.errors.invalidArgument (node:internal/deps/undici/undici:3575:28)
      at _Headers.set (node:internal/deps/undici/undici:8814:31)
      at fromNodeHeaders (MYPROJECT.yarn/__virtual__/@react-router-dev-virtual-86a2f80477/4/.yarn/berry/cache/@react-router-dev-npm-7.2.0-2cf33a3d6a-10c0.zip/node_modules/@react-router/dev/dist/vite.js:879:17)
      at fromNodeRequest (MYPROJECT/.yarn/__virtual__/@react-router-dev-virtual-86a2f80477/4/.yarn/berry/cache/@react-router-dev-npm-7.2.0-2cf33a3d6a-10c0.zip/node_modules/@react-router/dev/dist/vite.js:895:14)
      at nodeHandler (MYPROJECT/.yarn/__virtual__/@react-router-dev-virtual-86a2f80477/4/.yarn/berry/cache/@react-router-dev-npm-7.2.0-2cf33a3d6a-10c0.zip/node_modules/@react-router/dev/dist/vite.js:2768:30)
      at MYPROJECT/.yarn/__virtual__/@react-router-dev-virtual-86a2f80477/4/.yarn/berry/cache/@react-router-dev-npm-7.2.0-2cf33a3d6a-10c0.zip/node_modules/@react-router/dev/dist/vite.js:2775:23
      at processTicksAndRejections (node:internal/process/task_queues:105:5)
Enter fullscreen mode Exit fullscreen mode

This was traced back to the Vite SSL plugin used for authentication testing. The issue was resolved by either disabling SSL during development or adding proxy parameters to the Vite configuration:

// vite.config.ts

export default defineConfig({
  plugins: [
    reactRouter(),
    basicSsl({ // this is the cause of error
      name: "test",
      domains: ["*.custom.com"],
      certDir: "/Users/.../.devServer/cert",
    }),
  ],
 ...
  server: {
    proxy: {}, // this is solution
  },
});
Enter fullscreen mode Exit fullscreen mode

Further information on this issue can be found here: https://github.com/remix-run/remix/issues/10445

Client side logic issues

After including my <App /> into catchall.tsx (following migration guide) and running react-router dev I encountered an ERR_UNHANDLED_REJECTION in the client component:

node:internal/process/promises:392
      new UnhandledPromiseRejection(reason);
      ^
Enter fullscreen mode Exit fullscreen mode

Debugging with VS Code revealed a problem with Dexie, specifically a DexieError. This client-side code, used for accessing IndexedDB, was unexpectedly running on the server.
This occurred because the RR framework pre-renders even in spa-mode when ssr: false:

It's important to note that setting ssr:false only disables runtime server rendering. React Router will still server render your root route at build time to generate the index.html file. This is why your project still needs a dependency on @react-router/node and your routes need to be SSR-safe.
https://reactrouter.com/how-to/spa

// react-router.config.ts

export default {
  appDirectory: "src",
  ssr: false,
} satisfies Config;
Enter fullscreen mode Exit fullscreen mode

To resolve this I made my routes to be SSR-safe as mentioned in RR docs. In my case I just added if statement to function which is accessing IndexedDB
This was a part of my Redux store initialization logic.

// store.ts
if (typeof window !== "undefined" && !import.meta.env.SSR) {
  storeInitialize(store); // Function access indexedDB under the hood
}
Enter fullscreen mode Exit fullscreen mode

Setting routes (move away from catchAll.tsx)

With the migration guide completed, I had a functioning SPA with a single route (catchall.tsx).
To achieve my intended structure, I migrated my routes from the RR library:

// App.tsx
// Legacy config for Library mode
<BrowserRouter>
  <Routes>
    <Route path="/" element={<AppRootLayout />}>
      <Route path="book" element={<BookContainerLayout />}>
        <Route path="page/:uuid" element={<MainContainerLayout />} />
      </Route>
    </Route>
  </Routes>
</BrowserRouter>
Enter fullscreen mode Exit fullscreen mode

And configured the framework routes like this:

// routes.ts
// config for Framework mode
export default [
  route("/", "./features/landing-page/components/LandingPage.tsx"),
  route("/app", "./pages/AppRootLayout.tsx", [
    route("book", "./layouts/BookContainerLayout/BookContainerLayout.tsx", [
      route(
        "page/:uuid",
        "./layouts/MainContainerLayout/MainContainerLayout.tsx"
      ),
    ]),
  ]),
Enter fullscreen mode Exit fullscreen mode
// react-router.config.ts

export default {
  appDirectory: "src",
  ssr: false,
  async prerender() {
    return ["/"];
  },
} satisfies Config;
Enter fullscreen mode Exit fullscreen mode

Moving providers

After configuring my routes, I encountered errors related to my providers, such as:

Error: Error: could not find react-redux context value; please ensure the component is wrapped in a <Provider>
    at onShellError (MYAPP/src/entry.server.tsx:57:18)
Enter fullscreen mode Exit fullscreen mode

The question arose: where should these providers be placed?
Initially, they were defined within App.tsx:

function App() {
  return (
    <Auth0Provider
        ...
        authorizationParams={{
        redirect_uri: window.location.href, // notice client code here
      }}
    >
      <ReduxProvider store={store}>
        <AuthProvider>
            <AppRootLayout />
...
Enter fullscreen mode Exit fullscreen mode

Through trial and error, it was discovered that the providers functioned correctly
when placed within the component rendered on the /app route, which in my case was AppRootLayout.tsx.
To accommodate client-side code within the <Auth0Provider> props, an empty page was returned during server-side rendering
(though a loading state could be implemented for a better user experience):

// AppRootLayout.tsx

const [isClient, setIsClient] = useState<boolean>(false);
  useEffect(() => {
    setIsClient(true);
  }, []);

  if (!isClient) return null;

  return (
    <Auth0Provider
    ...
      authorizationParams={{
        redirect_uri: window.location.href,
      }}
    >
      <AuthProvider>
        <Provider store={store}>
          // AppRootLayout original content
Enter fullscreen mode Exit fullscreen mode

Applying css

Finally, I realized that CSS styles were missing in new files. To apply styling, the CSS files previously used in main.tsx needed to be imported into the newly created root.tsx:

// root.tsx

import "./index.css"; // these css where previously used in my `main.tsx`
import "./App.css";
...
Enter fullscreen mode Exit fullscreen mode

Serving Issues After Build: No Matching Routes Found

At this stage, the app functioned correctly during development with react-router dev.
Aafter building with react-router build, the following files were generated:

build/
    client/
        assets/
        __spa-fallback.html
        index.html
        multiple *.png
Enter fullscreen mode Exit fullscreen mode

Serving these files with sirv-cli build/client --single __spa-fallback.html, as recommended in the documentation (https://reactrouter.com/how-to/pre-rendering), resulted in the / route working as expected, but accessing /app/* produced the following console error:

No routes matched location "/app"
Enter fullscreen mode Exit fullscreen mode

This error originated from useRoutesImpl() in chunk-SYFQ2XB5-D-Hkp_oB.js:

warning(
  parentRoute || matches != null,
  `No routes matched location "${location.pathname}${location.search}${location.hash}"`
);
Enter fullscreen mode Exit fullscreen mode

The app displayed a fatal error message with no descriptive context:

app screenshot

After extensive troubleshooting, updating Vite and React Router resolved the issue:

yarn up vite
yarn up react-router
Enter fullscreen mode Exit fullscreen mode

Afterword

Migrating to the RR framework proved to be a time-consuming and challenging process. Had I known the extent of the effort required, I might have opted to create a separate project for the landing page.
I hope this article provides valuable insights and facilitates a smoother migration process for others.

Happy coding!

originally posted on my blog: https://philrich.dev

Top comments (0)