ProvenPin Install

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
pinned versions10
@tanstack/react-query@5.104.3react@19.3.1react-dom@19.3.1@types/node@24.19.2vitest@5.0.4@testing-library/react@16.3.3@testing-library/dom@10.4.2jsdom@30.1.3@types/react@19.3.0@types/react-dom@19.3.0
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
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.

package.json
{
  "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"
  }
}
vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({ esbuild: { jsx: "automatic" }, test: { environment: "jsdom" } });
src/Todos.tsx
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>
  );
}
test/Todos.test.tsx
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.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.