TanStack Query + React Testing Library hooks and components (Vitest + jsdom): working versions (tested 2026-10-15)
Test React components and hooks that use TanStack Query v5 with Testing Library on jsdom: fresh QueryClient per test, retries off, mocked fetchers, mutations with invalidation.
passed3 passed · 0 failed2026-10-15 13:33 UTC- Status
- passed
- Last passed
- 2026-10-15 13:33 UTC
- Tests
- 3 / 3
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38742087302
The code
Exactly the files that passed. Your agent can pull them with get_recipe("react-query-testing-library") (install), or see them on GitHub.
{
"name": "react-query-testing-library",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
},
"dependencies": {
"@tanstack/react-query": "5.104.3",
"react": "19.3.1",
"react-dom": "19.3.1"
},
"devDependencies": {
"@types/node": "24.19.2",
"vitest": "5.0.4",
"@testing-library/react": "16.3.3",
"@testing-library/dom": "10.4.2",
"jsdom": "30.1.3",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0"
}
}
import { defineConfig } from "vitest/config";
export default defineConfig({ esbuild: { jsx: "automatic" }, test: { environment: "jsdom" } });
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export type Api = { list: () => Promise<string[]>; add: (t: string) => Promise<void> };
export function useTodos(api: Api) {
return useQuery({ queryKey: ["todos"], queryFn: api.list });
}
export function Todos({ api }: { api: Api }) {
const qc = useQueryClient();
const todos = useTodos(api);
const add = useMutation({ mutationFn: api.add, onSuccess: () => qc.invalidateQueries({ queryKey: ["todos"] }) });
if (todos.isPending) return <p>Loading...</p>;
if (todos.isError) return <p role="alert">Error: {todos.error.message}</p>;
return (
<div>
<ul>{todos.data.map((t) => <li key={t}>{t}</li>)}</ul>
<button onClick={() => add.mutate("new")}>Add</button>
</div>
);
}
import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, fireEvent, renderHook, waitFor, cleanup } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
import { Todos, useTodos, type Api } from "../src/Todos";
afterEach(cleanup);
// New client per test, retries off, so failures show immediately and cache never leaks between tests.
function wrapper() {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } });
return ({ children }: { children: ReactNode }) => <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
}
describe("Todos", () => {
it("hook resolves data", async () => {
const api: Api = { list: vi.fn().mockResolvedValue(["a"]), add: vi.fn() };
const { result } = renderHook(() => useTodos(api), { wrapper: wrapper() });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual(["a"]);
});
it("renders the error state", async () => {
const api: Api = { list: vi.fn().mockRejectedValue(new Error("boom")), add: vi.fn() };
render(<Todos api={api} />, { wrapper: wrapper() });
expect(await screen.findByRole("alert")).toHaveProperty("textContent", "Error: boom");
});
it("mutation invalidates and refetches", async () => {
const items = ["a"];
const api: Api = { list: vi.fn(async () => [...items]), add: vi.fn(async (t: string) => { items.push(t); }) };
render(<Todos api={api} />, { wrapper: wrapper() });
await screen.findByText("a");
fireEvent.click(screen.getByText("Add"));
await screen.findByText("new");
expect(api.list).toHaveBeenCalledTimes(2);
});
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Jotai atoms with React 19, tested with Testing Library and isolated stores
Jotai 3 primitive, derived, write-only and async atoms driving a React 19 todo UI with Suspense, tested in Vitest on happy-dom with @testing-library/react, user-event and a fresh createStore per test, plus store-only tests without React.
Preact components with Signals state, tested with Testing Library on jsdom
A Preact 11 cart using @preact/signals signal and computed state plus a hooks-based async form, tested in Vitest on jsdom with @testing-library/preact and user-event, including re-render on signal change without new props.
SolidJS signals, memos and resources, tested with Solid Testing Library
SolidJS components using createSignal, createMemo, For, Show and createResource with Suspense, compiled by vite-plugin-solid and tested in Vitest on jsdom with @solidjs/testing-library and user-event.
Astro 7 component rendering tests with the Container API (Vitest)
Render Astro components to HTML in Vitest with the Container API and getViteConfig, checking props, slots and conditional markup.
All @tanstack/react-query recipesAll react recipesAll @testing-library/react recipesAll vitest recipesAll jsdom recipes