DEV Community

Cover image for Testing TanStack Router
Salva Torrubia
Salva Torrubia

Posted on • Edited on

Testing TanStack Router

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 }
}

Enter fullscreen mode Exit fullscreen mode

Create a Minimal Root Route

const rootRoute = createRootRoute({
  component: () => (
    <>
      <div data-testid="root-layout" />
      <Outlet />
    </>
  ),
})
Enter fullscreen mode Exit fullscreen mode
  • 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>,
})
Enter fullscreen mode Exit fullscreen mode
  • 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 />,
})
Enter fullscreen mode Exit fullscreen mode
  • 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,
})
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode
  • <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')
Enter fullscreen mode Exit fullscreen mode
  • 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 }
Enter fullscreen mode Exit fullscreen mode
  • 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()
})

Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
dakkers profile image
Dakota St. Laurent

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.