Lesson 20 / 25

Context, Providers and Routing

A custom render wrapper.

Render with the same providers as the app

Real components depend on context: a theme, an auth session, a data-fetching client such as TanStack Query, i18n and a router. Rendering them bare throws or behaves unrealistically. Write a custom render that wraps the UI in the app's providers via the wrapper option, accepts options such as an initial route or user, and re-exports everything from Testing Library, so tests import render from your test utils. Create a fresh store or query client per test to keep isolation (and turn off retries in tests so error states appear quickly). For routing, use an in-memory router such as React Router's MemoryRouter with initialEntries; package names differ between React Router versions, so check the docs for yours.

test-utils.tsx with a wrapper

Assumes React Router and TanStack Query; adapt to your stack.

// src/test/test-utils.tsx
import type { ReactElement, ReactNode } from 'react';
import { render, type RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { AuthProvider, type User } from '../auth';

type Options = Omit<RenderOptions, 'wrapper'> & { route?: string; user?: User | null };

export function renderWithProviders(ui: ReactElement, { route = '/', user = null, ...options }: Options = {}) {
  const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });

  function Wrapper({ children }: { children: ReactNode }) {
    return (
      <QueryClientProvider client={queryClient}>
        <AuthProvider initialUser={user}>
          <MemoryRouter initialEntries={[route]}>{children}</MemoryRouter>
        </AuthProvider>
      </QueryClientProvider>
    );
  }
  return render(ui, { wrapper: Wrapper, ...options });
}

export * from '@testing-library/react';

// in a test
// renderWithProviders(<AccountPage />, { route: '/account', user: buildUser({ name: 'Asha' }) });

A stage with the lights on

Rendering a component without its providers is like rehearsing a play in an empty warehouse with no lights or props. The custom render builds the real stage once, so every rehearsal looks like opening night.

Quick check: Why create a new QueryClient (or store) inside the custom render?

  • Because providers cannot be reused in production
  • So cached data and state do not leak from one test into another
  • To make tests run in a real browser
  • To increase coverage numbers
Answer

So cached data and state do not leak from one test into another — Fresh state per test keeps tests isolated.