Official documentation: https://reactrouter.com/upgrading/router-provider
-
Vite config. If the vite config contains a
root
property it needs to be removed. If you are running vitest coverage reports you may want to move it totest.root
or it will show coverage for the entire project. Any paths relative to root need to be adjusted as well, e.g. if you havepublicDir
set to../public
it should be set to justpublic
(or removed as that is the default). -
Vitest. You don't want the
reactRouter
plugin running for tests, so setup as
{
plugins: [
! process.env.VITEST && reactRouter()
]
}
- Imports. Even if you're running with ssr disabled, the bundling process still needs everything to be compatible with ssr. If you're seeing the error
Directory import ... is not supported
you can fix this by adding tossr.noExternal
in the vite config, e.g.
{
ssr {
noExternal: ['@apollo/client']
}
}
Similarly if you have any libraries that include css files imported by components within the library they will need to be added here as well.
- More Imports. If you're still having trouble importing libraries, the only solution I found that worked was to lazy load.
import { lazy, Suspense } from "react";
const SimpleMDE = lazy(() => import('react-simplemde-editor').then(pkg => ({ default: pkg.SimpleMdeReact })));
function Editor() {
return <Suspense><SimpleMDE /></Suspense>;
}
- Route Modules. When creating the route module files I initially exported the component like this ...
export { default } from "./Component";
This works while still using useLoaderData
, but fails to pass in the props when you switch to using the type system. To fix this it needs to be imported and then exported.
import Component from "./Component";
export default Component;
- Basename. If you need a basename it has to end with a "/" otherwise the asset links come out as
/fooassets/*
instead of/foo/assets/*
. Unfortunately this also prevents requests to/foo
from working. I wrote a vite plugin to fix the links.
function fixAssetPaths() {
let base;
let dir;
return {
name: 'fix-asset-paths',
outputOptions: (options) => {
// The react-router vite plugin will move the index from server to client when done, so we need to adjust the path.
dir = options.dir.replace(/server$/, 'client');
},
configResolved: config => {
base = config.rawBase;
},
closeBundle: async () => {
if (! dir) return;
if (base.endsWith('/')) return;
const index_file = path.join(dir, 'index.html');
if (! fs.existsSync(index_file)) return;
// Include a trailing slash in the match in case it does exist.
// With react-router 7.0.2 the initial issue was fixed but instead it just ignores base and sets everything to "/assets", to fix have set the base as optional and included the word assets in the regexp.
const base_re = new RegExp(`"(${base})?/?assets`, 'g');
const index = await fs.promises.readFile(index_file, 'utf-8');
const fixed_index = index.replace(base_re, `"${base}/assets`);
let fix_applied = index !== fixed_index;
await fs.promises.writeFile(index_file, fixed_index, 'utf-8');
const manifest_files = (await fs.promises.readdir(path.join(dir, 'assets'))).filter(n => n.startsWith('manifest'));
for (const file of manifest_files) {
const file_path = path.join(dir, 'assets', file);
const manifest = await fs.promises.readFile(file_path, 'utf-8');
const fixed_manifest = manifest.replace(base_re, `"${base}/assets`);
fix_applied = fix_applied || manifest !== fixed_manifest;
await fs.promises.writeFile(file_path, fixed_manifest, 'utf-8');
}
if (fix_applied) {
console.log("Asset links fixed.");
} else {
console.log("Asset links did not need fixing, you can remove this plugin.");
}
}
};
}
- Testing. With
RouterProvider
I would wrap my test components in a memory router that was passed the route objects I was interested in testing. This is no longer feasible using the framework as the route objects have been replaced with modules.
For testing loader, component, and action a utility function like this was helpful:
import { ComponentType } from 'react';
import { ActionFunctionArgs, ClientActionFunctionArgs, ClientLoaderFunctionArgs, createRoutesStub, LoaderFunctionArgs, UIMatch, useActionData, useLoaderData, useParams } from 'react-router';
export function createModuleStub<Props extends {
params: Params
loaderData: LData
actionData: AData
// eslint-disable-next-line @typescript-eslint/no-explicit-any
matches: any
}, Params extends Record<string, string>, LData = undefined, AData = undefined>(
Component: ComponentType<Props>,
options: {
path?: string
loader?: (args: LoaderFunctionArgs & {
params: Params
serverLoader: ClientLoaderFunctionArgs['serverLoader']
}) => Promise<LData> | LData
action?: (args: ActionFunctionArgs & {
params: Params
serverAction: ClientActionFunctionArgs['serverAction']
}) => Promise<AData> | AData
matches?: UIMatch[]
ErrorBoundary?: ComponentType
children?: Parameters<typeof createRoutesStub>[0]
routes?: Parameters<typeof createRoutesStub>[0]
} = {}
) {
return createRoutesStub([
{
path: options.path ?? '/',
loader: (args) => {
const { params, ...rest } = args;
return options.loader?.({
...rest,
params: params as Params,
serverLoader: (async () => null) as ClientLoaderFunctionArgs['serverLoader']
}) || null;
},
action: (args) => {
const { params, ...rest } = args;
return options.action?.({
...rest,
params: params as Params,
serverAction: (async () => null) as ClientActionFunctionArgs['serverAction']
}) || null;
},
children: options.children || [],
ErrorBoundary: options.ErrorBoundary,
Component: () => {
const params = useParams();
const loaderData = useLoaderData();
const actionData = useActionData();
const props = {
params,
loaderData,
actionData,
matches: options.matches || []
} as Props;
return <Component {...props} />;
}
},
...options.routes || []
]);
}
import { createModuleStub } from "./test-utils";
import ComponentToTest, { clientAction, clientLoader } from "./route";
const Stub = createModuleStub(ComponentToTest, {
loader: clientLoader,
action: clientAction
});
test('it works', () => {
render(<Stub />);
// Test stuff
});
For tests that involve multiple pages using browser tests may be more appropriate.