DEV Community

Cover image for Testing TanStack Router
Salva Torrubia
Salva Torrubia

Posted on • Edited on

2 1

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! 🚀

Modern auth, access management and billing for engineers.

Modern auth, access management and billing for engineers.

Secure and monetize your product from day one – with less code.

Get a free account

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.

Runner H image

Automate Your Workflow in Slack, Gmail, Notion & more

Runner H connects to your favorite tools and handles repetitive tasks for you. Save hours daily. Try it free while it’s in beta.

Try for Free

đź‘‹ Kindness is contagious

Explore this insightful write-up embraced by the inclusive DEV Community. Tech enthusiasts of all skill levels can contribute insights and expand our shared knowledge.

Spreading a simple "thank you" uplifts creators—let them know your thoughts in the discussion below!

At DEV, collaborative learning fuels growth and forges stronger connections. If this piece resonated with you, a brief note of thanks goes a long way.

Okay