pnpm add -D @tanstack-router-testing/react-router-testing @tanstack-router-testing/react-start-testingYou also need the usual peer dependencies: @tanstack/react-router, react, react-dom, vitest, and @testing-library/react.
If your app uses TanStack Start, add the tanstackStartTesting() Vite plugin. It wires up route-tree generation and aliases @tanstack/react-start to an in-process test shim so server functions run directly without a network layer.
// vitest.config.ts
import { tanstackStartTesting } from '@tanstack-router-testing/react-start-testing/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [tanstackStartTesting()],
test: {
environment: 'jsdom',
},
});If you only use TanStack Router (no Start), skip the plugin — no special Vitest config is needed.
Create a route tree, spin up a harness, load routes, navigate, and assert on loader data — all against a real router instance with memory history.
import { createRootRoute, createRoute, Outlet } from '@tanstack/react-router';
import { render } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { createRouterHarness } from '@tanstack-router-testing/react-router-testing';
const rootRoute = createRootRoute({
component: () => <Outlet />,
});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
loader: () => ({ message: 'Hello from index' }),
component: () => <h1>Home</h1>,
});
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
loader: () => ({ title: 'About Us' }),
component: () => <h1>About</h1>,
});
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]);
describe('router harness', () => {
it('loads the index route and reads loader data', async () => {
const harness = createRouterHarness({ routeTree });
await harness.load();
expect(harness.getLoaderData('/')).toEqual({ message: 'Hello from index' });
const { getByText } = render(<harness.TestRouterProvider />);
expect(getByText('Home')).toBeDefined();
harness.cleanup();
});
it('navigates to /about and asserts on the new route', async () => {
const harness = createRouterHarness({ routeTree });
await harness.load();
await harness.navigate({ to: '/about' });
expect(harness.getLoaderData('/about')).toEqual({ title: 'About Us' });
expect(harness.getParams('/about')).toEqual({});
harness.cleanup();
});
});If your app uses file-based routing, you can test a single route file directly — no need to build a route tree by hand. Import the route, pass it to createRouterHarness, and get fully typed params and search.
import { render } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { createRouterHarness } from '@tanstack-router-testing/react-router-testing';
import { Route } from './routes/posts.$postId';
describe('post route', () => {
it('loads and renders a post', async () => {
const harness = createRouterHarness({
route: Route,
params: { postId: '42' }, // typed from the route's path
});
await harness.load();
expect(harness.getLoaderData(Route)).toBeDefined();
const { findByText } = render(<harness.TestRouterProvider />);
await expect(findByText('Post 42')).resolves.toBeTruthy();
harness.cleanup();
});
});The harness automatically:
- Walks from the route to its root to get the full route tree
- Neuters ancestor loaders for isolation (ancestor
beforeLoadstill runs for context cascading) - Computes the initial URL from
paramsandsearch
You can also override loader data to skip the real loader entirely:
const harness = createRouterHarness({
route: Route,
params: { postId: '42' },
loaderData: { id: '42', title: 'Stubbed Post' },
});Or test independent components that call Route.useLoaderData():
const { TestRouterProvider } = createRouterHarness({
route: Route,
params: { postId: '42' },
});
render(
<TestRouterProvider>
<MyComponent /> {/* calls Route.useLoaderData() internally */}
</TestRouterProvider>,
);Note: The Vite plugin (
tanstackStartTesting()) auto-injectsrouteTree.gen.tsas a Vitest setup file, so all routes have their parent/path/id wired up before any test runs. If you're not using the plugin, importrouteTree.gen.tsat the top of your test file for side effects.
Add a setup file to your Vitest config and harnesses are cleaned up automatically after each test — no manual afterEach needed.
TanStack Start projects (cleans both harnesses and server function mocks):
// vitest.config.ts
export default defineConfig({
plugins: [tanstackStartTesting()],
test: {
environment: 'jsdom',
setupFiles: ['@tanstack-router-testing/react-start-testing/cleanup'],
},
});Router-only projects (cleans harnesses only):
// vitest.config.ts
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['@tanstack-router-testing/react-router-testing/cleanup'],
},
});Note:
react-start-testing/cleanupincludes router cleanup transitively — you don't need both.
If your linter enforces vitest/require-top-level-describe, the bare afterEach in cleanup setup files may trigger a warning. Suppress it for setup files:
// eslint.config.js (flat config)
{
files: ['vitest.setup.ts', '**/setup/*.ts'],
rules: {
'vitest/require-top-level-describe': 'off',
},
}Alternatively, ensure your lint include/exclude patterns don't cover Vitest setup files — they're not test files and the rule should not apply to them.
With auto-cleanup, tests become simpler:
it('loads the root', async () => {
const harness = createRouterHarness({ routeTree });
await harness.load();
expect(harness.getMatch('/')).toBeDefined();
// no cleanup needed — handled by the setup file
});If you prefer manual cleanup over setup files, clean up the harness in afterEach so history listeners don't leak between tests:
import { afterEach, describe, expect, it } from 'vitest';
import { createRouterHarness } from '@tanstack-router-testing/react-router-testing';
describe('my routes', () => {
let harness: ReturnType<typeof createRouterHarness>;
afterEach(() => {
harness.cleanup();
});
it('loads the root', async () => {
harness = createRouterHarness({ routeTree });
await harness.load();
expect(harness.getMatch('/')).toBeDefined();
});
});When testing server functions, use mockServerFn to replace the real handler and clearStartMocks to tear down all mocks between tests:
import { afterEach, describe, expect, it } from 'vitest';
import { clearStartMocks, mockServerFn } from '@tanstack-router-testing/react-start-testing';
import { getUser } from '../server/getUser';
describe('getUser server function', () => {
afterEach(() => {
clearStartMocks();
});
it('returns mocked user data', async () => {
mockServerFn(getUser, async ({ data }) => ({
id: data.userId,
name: 'Test User',
}));
const result = await getUser({ data: { userId: '42' } });
expect(result).toEqual({ id: '42', name: 'Test User' });
});
});mockServerFn returns a disposer if you prefer per-test teardown instead of clearStartMocks:
const dispose = mockServerFn(getUser, async () => ({ id: '1', name: 'Mock' }));
// ... test ...
dispose();The Start package also exports mockMiddleware for overriding middleware phases, and runInStartEnv for controlling the server/client environment flag. See the react-start-testing package for the full API.
- Browse the
docs/guides/anddocs/examples/directories as they're populated. - Read the JSDoc on each export — every public function and type has
@param,@returns, and@exampleannotations. - Check the package READMEs:
react-router-testing,react-start-testing.