Vitest Snapshot Testing for React
How to use Vitest snapshot testing to catch unintended UI changes in React components, including inline snapshots and snapshot update workflows.
Overview
Snapshot testing captures the rendered output of a component at a point in time. On subsequent runs, Vitest compares the current output against the stored snapshot and fails if they differ — alerting you to unintended UI changes. Vitest is a Jest-compatible test runner for Vite projects with built-in snapshot support. If you’re coming from Jest, the API is nearly identical; check our Jest snapshot testing recipe for the differences, and the Snapshot Testing pattern for the framework-agnostic pattern behind both. For a deeper dive into the full Vitest setup, see the complete Vitest + React testing guide.
When to Use
- Catching accidental CSS or markup regressions in presentational components.
- Verifying that a component renders the same structure across refactors.
- Testing components with stable, deterministic output (no random IDs or timestamps).
- Documenting the expected output of utility functions that return complex objects.
When NOT to Use
- Components with dynamic content (dates, random values, UUIDs) — snapshots will always fail.
- Testing business logic — use unit tests with explicit assertions instead.
- Components that change frequently during active development — snapshot churn is noise.
- Testing accessibility or interaction — snapshots only check rendered HTML, not behavior.
Solution
Setup
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./test/setup.ts"],
},
});
// test/setup.ts
import "@testing-library/jest-dom";
Basic snapshot test
import { render } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Button } from "./Button";
describe("Button", () => {
it("matches snapshot for default variant", () => {
const { container } = render(<Button>Click me</Button>);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot for primary variant", () => {
const { container } = render(<Button variant="primary">Save</Button>);
expect(container.firstChild).toMatchSnapshot();
});
it("matches snapshot for disabled state", () => {
const { container } = render(<Button disabled>Disabled</Button>);
expect(container.firstChild).toMatchSnapshot();
});
});
Inline snapshot
import { render } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Badge } from "./Badge";
describe("Badge", () => {
it("renders inline snapshot", () => {
const { container } = render(<Badge count={5} />);
expect(container.firstChild).toMatchInlineSnapshot(`
<span
class="badge ri"
>
5
</span>
`);
});
});
Snapshot with property matchers
import { describe, it, expect } from "vitest";
import { buildApiResponse } from "./api";
describe("buildApiResponse", () => {
it("matches snapshot ignoring dynamic date", () => {
const response = buildApiResponse({ data: [1, 2, 3], status: 200 });
expect(response).toMatchSnapshot({
timestamp: expect.any(String),
reportId: expect.any(String),
});
});
});
Snapshot a function output
import { describe, it, expect } from "vitest";
import { formatCurrency } from "./format";
describe("formatCurrency", () => {
it("matches snapshot for USD", () => {
expect(formatCurrency(1234.56, "USD")).toMatchInlineSnapshot(
`"$1,234.56"`,
);
});
it("matches snapshot for EUR", () => {
expect(formatCurrency(1234.56, "EUR")).toMatchInlineSnapshot(
`"€1,234.56"`,
);
});
});
Async component snapshot
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { UserProfile } from "./UserProfile";
describe("UserProfile", () => {
it("matches snapshot after data loads", async () => {
render(<UserProfile userId={42} />);
await screen.findByRole("heading", { name: /alice/i });
expect(document.body).toMatchSnapshot();
});
});
Explanation
toMatchSnapshot() serializes the value the first time it runs and writes it to a .snap
file. On later runs, Vitest compares the serialized value to the stored copy. Use
toMatchInlineSnapshot() to keep the expected value in the test file, which makes reviews
easier. Property matchers let you skip dynamic fields — you check the type, not the exact value.
I reach for inline snapshots when the output is small enough to read in a PR diff — a
formatted string, a small config object, a single component’s HTML. For anything larger,
.snap files keep the test file readable, but you lose the at-a-glance reviewability. The
trade-off is real: inline snapshots bloat the test file, but .snap files are easy to skip
in code review because they’re auto-generated.
One thing that bit me: snapshots don’t catch everything. A component can
render the same HTML but behave differently — a button that stopped firing onClick, a
form that submits to the wrong endpoint, a focus trap that breaks keyboard navigation.
Snapshots verify structure, not behavior. Pair them with
@testing-library/react interaction tests
for real coverage.
Variants
Snapshot with mock calls
import { render, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { SubmitForm } from "./SubmitForm";
describe("SubmitForm", () => {
it("calls onSubmit with form data", () => {
const onSubmit = vi.fn();
const { getByRole } = render(<SubmitForm onSubmit={onSubmit} />);
fireEvent.click(getByRole("button", { name: /submit/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: "",
password: "",
});
expect(onSubmit.mock.calls).toMatchSnapshot();
});
});
RSC snapshot
For React Server Components, render the output to a string before snapshotting:
import { renderToString } from "react-dom/server";
import { describe, it, expect } from "vitest";
import { StaticPage } from "./StaticPage";
describe("StaticPage", () => {
it("matches server snapshot", () => {
const html = renderToString(<StaticPage />);
expect(html).toMatchSnapshot();
});
});
Best Practices
- Keep snapshots small — snapshot a single component, not an entire page tree. I’ve been bitten by page-level snapshots that break on every CSS tweak.
- Use inline snapshots for small, stable outputs so they’re reviewable in PRs.
- Review snapshot diffs in PRs — a green test with a changed snapshot means someone
approved the change. I block PRs where
.snapfiles change without a comment explaining why. - Use property matchers for dynamic fields —
expect.any(String)orexpect.any(Date)saves you from timestamp flakiness. - Run
vitest -uto update snapshots only after verifying the change is intentional. - Don’t snapshot components with random IDs, timestamps, or generated classes.
Common Mistakes
- Blindly updating snapshots — running
vitest -uwithout reviewing the diff hides real regressions. - Snapshotting too much — a 200-line snapshot is unreadable and fails on any minor CSS change. Break it into smaller component snapshots.
- Not using property matchers for dynamic data — without them, a timestamp in the output breaks the snapshot every single run.
- Storing snapshots away from tests — keep
.snapfiles next to test files for discoverability. - Using snapshots as the only test — snapshots verify structure, not behavior. Add
interaction tests with
@testing-library/react.
See Also
- Vitest Snapshot documentation — official guide
covering
toMatchSnapshot,toMatchInlineSnapshot, and snapshot settings. - React Testing Library docs — the recommended way to render React components in tests.
- Jest snapshot testing — the original snapshot testing API that Vitest extends.
- Jest snapshot testing recipe — our recipe for Jest projects that haven’t migrated to Vitest.
- Vitest + React complete guide — full setup including mocking, coverage, and CI integration.
Frequently Asked Questions
How do I update snapshots after an intentional change?
Run npx vitest -u (or --update). This regenerates all .snap files. Review the diff in
Git before committing.
What is the difference between toMatchSnapshot and toMatchInlineSnapshot?
toMatchSnapshot writes to a separate .snap file. toMatchInlineSnapshot writes the
snapshot directly in the test file as a string literal. Inline snapshots are more
reviewable in PRs but can bloat the test file for large outputs.
How do I ignore dynamic values in snapshots?
Use property matchers:
expect(result).toMatchSnapshot({
id: expect.any(String),
createdAt: expect.any(String),
});
Vitest will match the structure but ignore the actual values of those fields.
Should I commit .snap files to Git?
Yes. Snapshot files should be committed and reviewed in PRs. They serve as a contract for the expected output.
Can I use snapshot testing with React Server Components?
Yes. Use renderToString from react-dom/server and snapshot the HTML output. Client-side
rendering tests use @testing-library/react as usual.
Why does snapshot drift happen in large test suites?
Use toMatchInlineSnapshot for small outputs so the expected value is visible in code
review. For .snap files, enable --ci in CI to fail on outdated snapshots instead of
silently writing new ones. Run vitest -u only locally after verifying the change is
intentional.
Related Resources
Snapshot Testing React Components with Jest
How to use Jest snapshot testing to catch unintended UI regressions in React components and prevent visual bugs from reaching production
RecipeTest Express APIs with supertest
How to test Express.js REST API endpoints end-to-end using supertest, including status codes, JSON bodies, headers, authentication, and error handling.
RecipeWhen to Use useMemo and useCallback
How and when to use React's useMemo and useCallback hooks for performance optimization, and when they add unnecessary overhead.
GuideVitest for React: Component, Hook, and Integration Testing
Master Vitest for React testing: component tests with Testing Library, hook tests with renderHook, integration tests, mocking, snapshot testing, and parallel execution.
RecipeMock Network Requests with MSW
How to use Mock Service Worker (MSW) to intercept network requests in JavaScript tests and development, including REST and GraphQL mocking.
RecipeGenerate Test Data (Faker + Factories)
How to generate realistic, deterministic test data with Faker, factory-boy, and type-aware generators for reliable test suites in Python, JavaScript, and Java.