Migrating to React Router Framework: Challenges Beyond the Official Guide Migrating to React Router Framework: Challenges Beyond the Official Guide

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>

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:

Terminal window
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'

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:

Terminal window
[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)

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
},
});

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:

Terminal window
node:internal/process/promises:392
new UnhandledPromiseRejection(reason);
^

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;

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
}

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>

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"
),
]),
]),
react-router.config.ts
export default {
appDirectory: "src",
ssr: false,
async prerender() {
return ["/"];
},
} satisfies Config;

Moving providers

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

Terminal window
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)

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 />
...

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

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";
...

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

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"

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}"`
);

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

app screenshot

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

Terminal window
yarn up vite
yarn up react-router

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!


← Back to home