How to Test React Query in React

Testing components that use React Query (TanStack Query) requires a specific approach because of how it manages asynchronous state and caching. This article provides a direct, step-by-step guide on how to test React Query in your React applications. You will learn how to configure a custom test renderer with QueryClientProvider, mock network requests using Mock Service Worker (MSW), and write clean, reliable unit tests for components and custom hooks.

1. Set Up the QueryClientProvider Wrapper

React Query hooks like useQuery and useMutation must be rendered within a QueryClientProvider. To avoid repeating this setup in every test, create a reusable wrapper function using React Testing Library.

It is crucial to create a new QueryClient instance for every single test to prevent test cross-contamination caused by cached data. Additionally, disabling retries in your test configuration prevents Jest or Vitest from timing out when simulating failed requests.

import React from 'react';
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const createTestQueryClient = () => new QueryClient({
  defaultOptions: {
    queries: {
      retry: false, // Prevents Jest from timing out on failed queries
    },
  },
});

export function renderWithClient(ui) {
  const testQueryClient = createTestQueryClient();
  const { rerender, ...result } = render(
    <QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider>
  );
  return {
    ...result,
    rerender: (rerenderUi) =>
      rerender(
        <QueryClientProvider client={testQueryClient}>{rerenderUi}</QueryClientProvider>
      ),
  };
}

2. Mock Network Requests with MSW

Instead of mocking the useQuery hook itself, mock the network layer. This ensures you are testing the actual behavior of your component. Mock Service Worker (MSW) is the recommended tool for intercepting network requests in tests.

First, set up your MSW handlers to mock the API response:

import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('https://api.example.com/data', () => {
    return HttpResponse.json({ id: 1, name: 'Test Product' });
  }),
];

export const server = setupServer(...handlers);

Configure your test setup file (e.g., setupTests.js) to start the server before your tests run, and clean up afterward:

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

3. Testing a Component

With the wrapper and network mocks in place, you can test components that use React Query. Use findBy queries from React Testing Library to handle the asynchronous nature of the state changes.

Here is an example component and its test:

// ProductComponent.jsx
import React from 'react';
import { useQuery } from '@tanstack/react-query';

export function ProductComponent() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['product'],
    queryFn: () => fetch('https://api.example.com/data').then((res) => res.json()),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading data</div>;
  return <h1>{data.name}</h1>;
}
// ProductComponent.test.jsx
import React from 'react';
import { screen } from '@testing-library/react';
import { renderWithClient } from './utils';
import { ProductComponent } from './ProductComponent';
import { server } from './mocks/server';
import { http, HttpResponse } from 'msw';

describe('ProductComponent', () => {
  it('renders loading state initially, then displays data', async () => {
    renderWithClient(<ProductComponent />);

    // Assert loading state
    expect(screen.getByText('Loading...')).toBeInTheDocument();

    // Assert success state with fetched data
    const heading = await screen.findByRole('heading', { name: 'Test Product' });
    expect(heading).toBeInTheDocument();
  });

  it('renders error state on API failure', async () => {
    // Override the handler for this specific test
    server.use(
      http.get('https://api.example.com/data', () => {
        return new HttpResponse(null, { status: 500 });
      })
    );

    renderWithClient(<ProductComponent />);

    const errorMessage = await screen.findByText('Error loading data');
    expect(errorMessage).toBeInTheDocument();
  });
});

4. Testing Custom Hooks

If you encapsulate your React Query logic in custom hooks, you can test them directly using renderHook from React Testing Library. Pass the QueryClientProvider wrapper inside the options object.

import { renderHook, waitFor } from '@testing-library/react';
import { useQuery } from '@tanstack/react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

function useCustomHook() {
  return useQuery({
    queryKey: ['customData'],
    queryFn: () => fetch('https://api.example.com/data').then((res) => res.json()),
  });
}

const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

test('should fetch and return data in custom hook', async () => {
  const { result } = renderHook(() => useCustomHook(), {
    wrapper: createWrapper(),
  });

  await waitFor(() => expect(result.current.isSuccess).toBe(true));

  expect(result.current.data).toEqual({ id: 1, name: 'Test Product' });
});