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:
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:
[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:
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:
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:
After extensive troubleshooting, updating Vite and React Router resolved the issue:
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!
originally posted on my blog: https://philrich.dev
Top comments (0)