For my new project I decided to jump from React Router 7 to TanStack Router 🔥, especially since I’d been loving TanStack Query and figured the synergy between the two would be really powerful. But when it came time to write tests, I couldn’t find much documentation on wiring up TanStack Router in React Testing Library. That’s why I built this tiny helper 🛠️
Its a file-based implementation
Full Code
test-utils.ts
import React from 'react'
import {
Outlet,
RouterProvider,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
} from '@tanstack/react-router'
import { render, screen } from '@testing-library/react'
import { QueryClientProvider, QueryClient } from '@tanstack/react-query'
type RenderOptions = {
pathPattern: string
initialEntry?: string
queryClient?: QueryClient
}
/**
* Renders a component under:
* - a minimal TanStack Router instance (memory history),
* - optionally wrapped in a QueryClientProvider.
*
* If `initialEntry` is omitted, it defaults to `pathPattern`.
*
* @param Component The React component to mount.
* @param opts Render options.
* @returns { router, renderResult }
*/
export async function renderWithProviders(
Component: React.ComponentType,
{ pathPattern, initialEntry = pathPattern, queryClient }: RenderOptions,
) {
// Root route with minimal Outlet for rendering child routes
const rootRoute = createRootRoute({
component: () => (
<>
<div data-testid="root-layout"></div>
<Outlet />
</>
),
})
// Index route so '/' always matches
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <div>Index</div>,
})
// Test route mounting your Component at the dynamic path
const testRoute = createRoute({
getParentRoute: () => rootRoute,
path: pathPattern,
component: () => <Component />,
})
// Create the router instance with memory history
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, testRoute]),
history: createMemoryHistory({ initialEntries: [initialEntry] }),
defaultPendingMinMs: 0,
})
// Build the render tree and add QueryClientProvider if provided
let tree = <RouterProvider router={router} />
if (queryClient) {
tree = (
<QueryClientProvider client={queryClient}>{tree}</QueryClientProvider>
)
}
// Render and wait for the route to resolve and the component to mount
const renderResult = render(tree)
await screen.findByTestId('root-layout')
return { router, renderResult }
}
Create a Minimal Root Route
const rootRoute = createRootRoute({
component: () => (
<>
<div data-testid="root-layout" />
<Outlet />
</>
),
})
- Renders a
<div data-testid="root-layout"/>
plus an<Outlet/>
so we can wait for router hydration.
Add an Index Route (/)
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <div>Index</div>,
})
- Simple placeholder for index.
- Ensures that navigating to "/" always resolves without errors.
Add Your Test Route (path)
const testRoute = createRoute({
getParentRoute: () => rootRoute,
path: pathPattern,
component: () => <Component />,
})
-
pathPattern
may include dynamic segments (e.g. "/users/$userId"). - The router uses this pattern to know where to “mount” the component under test.
Instantiate the Router
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, testRoute]),
history: createMemoryHistory({ initialEntries: [initialEntry] }),
defaultPendingMinMs: 0,
})
-
routeTree
: Combines rootRoute with its children (indexRoute, testRoute). -
createMemoryHistory
: Starts the router at the desired path. -
initialEntries: [initialEntry]
: starts the router, keeping route definition (pathPattern
) separate from the test URL (initialEntry
). -
defaultPendingMinMs: 0
: Speeds up transition resolution during testing by removing any artificial delay.
Build the Render Tree
let tree = <RouterProvider router={router}/>
if (queryClient)
tree = <QueryClientProvider client={queryClient}>{tree}</QueryClientProvider>
-
<RouterProvider>
: Supplies the router context to your components. - Optional Query Client: If you passed queryClient, wrap the router in so your components can use React Query hooks.
Render and Await Hydration
const renderResult = render(tree)
await screen.findByTestId('root-layout')
-
render(tree)
: Uses React Testing Library to render the component tree. -
findByTestId('root-layout')
: Waits until the router’s root layout is mounted, guaranteeing that navigation and route resolution are complete before assertions.
Return Utilities for Tests
return { router, renderResult }
- router: Access the router instance in your test for navigation, state inspection, etc.
- renderResult: Provides all the usual Testing Library query utilities and can be usefull for testing accesibility
Example Usage with dynamic route
import { renderWithProviders } from './test-utils'
import { QueryClient } from '@tanstack/react-query'
import User from './User'
import { screen } from '@testing-library/react'
import { usersQueryOptions } from './queries/users/users'
// Optional
import { axe, toHaveNoViolations } from 'jest-axe'
type UserData = {
id: string
name: string
}
const testUserId = '1'
const pathPattern = '/users/$userId'
const initialEntry = `/users/${testUserId}`
const mockUser: UserData = { id: testUserId, name: 'Alice' }
test('it shows the user name', async () => {
const queryClient = new QueryClient()
// Prime the React-Query cache
queryClient.setQueryData(usersQueryOptions.queryKey, mockUser)
// Render
const { renderResult } = await renderWithProviders(User, {
pathPattern,
initialEntry,
queryClient,
})
// Assert that the user name is displayed
expect(screen.getByText(mockUser.name)).toBeInTheDocument()
// Optional: Accessibility audit
const results = await axe(renderResult.container)
expect(results).toHaveNoViolations()
})
In Summary
Thanks for reading! 🎉 I hope this helper makes your TanStack Router tests a breeze. If you have ideas, questions, or improvements, drop a comment below—let’s keep learning together. Happy testing! 🚀
Top comments (1)
I made an account just to say thanks for writing this post. I was trying to use Vitest to mock tanstack router's hooks but that didn't work out. This is much simpler.