Snapshot Testing in React Native: When It Helps and When It Hurts
Snapshot testing in React Native can catch unintended UI regressions, but done wrong it becomes a rubber-stamp exercise. This post covers when snapshots add value, how to write focused inline snapshots, and patterns that keep your snapshot suite meaningful over time.
Snapshot testing is one of the most misused testing techniques in React Native. Teams add it everywhere because it's trivially easy to write — expect(tree).toMatchSnapshot() — and then spend the next six months blindly updating snapshots whenever anything changes. At that point, the tests are waste.
Done right, snapshot testing is a lightweight guard against unintended rendering regressions. Done wrong, it's noise that trains your team to ignore test failures.
What Snapshot Testing Actually Does
A snapshot test renders a component to a serializable representation (the virtual DOM tree, or JSON), saves it to a .snap file on the first run, and compares on every subsequent run. If the output changes, the test fails.
import React from 'react';
import { render } from '@testing-library/react-native';
import { Badge } from '../Badge';
test('renders default badge', () => {
const { toJSON } = render(<Badge label="New" />);
expect(toJSON()).toMatchSnapshot();
});First run creates __snapshots__/Badge.test.tsx.snap:
exports[`renders default badge 1`] = `
<View
style={
{
"backgroundColor": "#3B82F6",
"borderRadius": 12,
"paddingHorizontal": 8,
"paddingVertical": 4,
}
}
>
<Text
style={
{
"color": "#FFFFFF",
"fontSize": 12,
"fontWeight": "600",
}
}
>
New
</Text>
</View>
`;Subsequent runs compare against this. Change the border radius accidentally? Test fails. Intentionally update the design? Run jest --updateSnapshot (-u) to regenerate.
When Snapshots Are Actually Useful
Snapshots are valuable in a narrow set of scenarios:
Pure presentational components with clear visual contracts. A Badge, Avatar, Tag, or Icon component that maps props to a predictable visual output. These rarely change and a snapshot makes regressions obvious.
Components with many conditional rendering branches. Snapshots cheaply document every if/else rendering path without writing individual assertions for each element.
Design system tokens. If your Button component applies specific colors, radii, and spacing from a design token system, a snapshot will catch when a token value changes unexpectedly.
What they're bad for: any component that interacts with APIs, navigation, or external state. Snapshots don't tell you whether the component works — only whether it looks the same as before.
The Problem With Large Snapshots
A 300-line snapshot of your HomeScreen component is useless. When it fails, you have to diff 300 lines to find the one meaningful change. Teams start running jest -u reflexively without reading the diff. The snapshot becomes a lie that always passes.
Rule of thumb: if you can't read the entire snapshot in 10 seconds and understand whether the change is correct, it's too large.
Inline Snapshots — The Better Default
Inline snapshots embed the expected output directly in the test file. This forces you to think about what you're snapshotting before you write the test, and makes diffs visible in code review without opening a .snap file.
import { render } from '@testing-library/react-native';
import { StatusBadge } from '../StatusBadge';
test('renders success state', () => {
const { toJSON } = render(<StatusBadge status="success" />);
expect(toJSON()).toMatchInlineSnapshot(`
<View
style={{"backgroundColor": "#22C55E", "borderRadius": 4, "padding": 4}}
>
<Text style={{"color": "#FFFFFF"}}>
Success
</Text>
</View>
`);
});
test('renders error state', () => {
const { toJSON } = render(<StatusBadge status="error" />);
expect(toJSON()).toMatchInlineSnapshot(`
<View
style={{"backgroundColor": "#EF4444", "borderRadius": 4, "padding": 4}}
>
<Text style={{"color": "#FFFFFF"}}>
Error
</Text>
</View>
`);
});Inline snapshots auto-populate on first run and auto-update with jest -u, just like external snapshots. The difference is visibility: a reviewer sees exactly what changed in the PR diff.
Focused Snapshots — Testing Specific Subtrees
Instead of snapshotting the entire component tree, snapshot only the part you care about:
import { render, screen } from '@testing-library/react-native';
import { ProductCard } from '../ProductCard';
test('price section renders correctly for discounted item', () => {
render(
<ProductCard
name="Widget"
price={19.99}
originalPrice={29.99}
discountPercent={33}
/>
);
// Only snapshot the price section, not the entire card
expect(screen.getByTestId('price-section')).toMatchSnapshot();
});This makes the snapshot smaller, more focused, and dramatically less likely to fail for unrelated reasons (like a change to the product image section).
Combining Snapshots with Behavioral Tests
The most effective pattern: use snapshots for structure and visual state, behavioral tests for interactions.
import { render, screen } from '@testing-library/react-native';
import userEvent from '@testing-library/user-event';
import { ToggleSwitch } from '../ToggleSwitch';
// Snapshot: captures the visual appearance of both states
test('matches snapshot when off', () => {
const { toJSON } = render(<ToggleSwitch value={false} onValueChange={() => {}} />);
expect(toJSON()).toMatchInlineSnapshot(`
<View style={{"backgroundColor": "#D1D5DB", "borderRadius": 16, "height": 32, "width": 56}}>
<View style={{"backgroundColor": "#FFFFFF", "borderRadius": 12, "height": 24, "left": 4, "position": "absolute", "top": 4, "width": 24}} />
</View>
`);
});
test('matches snapshot when on', () => {
const { toJSON } = render(<ToggleSwitch value={true} onValueChange={() => {}} />);
expect(toJSON()).toMatchInlineSnapshot(`
<View style={{"backgroundColor": "#3B82F6", "borderRadius": 16, "height": 32, "width": 56}}>
<View style={{"backgroundColor": "#FFFFFF", "borderRadius": 12, "height": 24, "position": "absolute", "right": 4, "top": 4, "width": 24}} />
</View>
`);
});
// Behavioral: verifies the toggle actually fires the callback
test('calls onValueChange when tapped', async () => {
const mockChange = jest.fn();
const user = userEvent.setup();
render(<ToggleSwitch value={false} onValueChange={mockChange} />);
await user.press(screen.getByRole('switch'));
expect(mockChange).toHaveBeenCalledWith(true);
});Handling Dynamic Content
Dynamic content like dates, IDs, and random values will break snapshots on every run. Freeze or replace them:
// Freeze Date
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-06-02T12:00:00Z'));
});
afterAll(() => {
jest.useRealTimers();
});
test('activity feed shows correct timestamp', () => {
const { toJSON } = render(
<ActivityItem timestamp={new Date('2026-06-02T11:00:00Z')} text="Item updated" />
);
expect(toJSON()).toMatchSnapshot();
});For random IDs, use deterministic values in tests:
// Mock uuid
jest.mock('uuid', () => ({ v4: () => 'test-uuid-1234' }));Snapshot Serializers
The default JSON serializer is verbose. For styled components or complex style objects, custom serializers make snapshots more readable.
Install jest-snapshot-serializer-raw for raw string snapshots, or write a custom one:
// Custom serializer that omits style objects (focus on structure only)
expect.addSnapshotSerializer({
test: (val) => val && val.props !== undefined,
print: (val, serialize) => {
const { style: _style, ...rest } = val.props ?? {};
return serialize({ ...val, props: rest });
},
});For React Native, @testing-library/react-native's toJSON() already produces a readable tree, but you can prune it further:
function stripStyles(node: any): any {
if (!node) return node;
const { style: _style, ...props } = node.props ?? {};
return {
...node,
props,
children: node.children?.map(stripStyles),
};
}
test('button structure without styles', () => {
const { toJSON } = render(<PrimaryButton label="Click me" />);
expect(stripStyles(toJSON())).toMatchSnapshot();
});When to Update vs When to Fix
When a snapshot test fails, ask one question: was this change intentional?
- Intentional redesign → update the snapshot (
jest -u), review the diff in code review - Unintentional regression → fix the component code, not the snapshot
The failure is only valuable if you read the diff. Make this part of your PR review checklist: any PR that updates .snap files should include an explanation in the description.
Keeping the Snapshot Suite Healthy
Delete obsolete snapshots. Run jest --ci to catch snapshots with no corresponding test. Use jest --verbose to see which snapshots are obsolete, then delete them.
Keep snapshot files in version control. The .snap files are the source of truth. They must be committed.
Avoid snapshots for frequently-changing components. If a component changes every sprint, its snapshot fails every sprint. Use behavioral tests instead.
Limit snapshot test files. In a large codebase, keeping snapshot test files to design system primitives (buttons, inputs, cards, badges) and avoiding them for business logic components is a sustainable pattern.
CI snapshot check. In CI, run Jest with --ci flag which fails if snapshots are missing rather than creating them:
- name: Run tests
run: jest --ci --coverageThis catches developers who forget to commit updated snapshots.
A Practical Policy
Here's a snapshot policy that works at scale:
- Design system components (Button, Input, Badge, Card, etc.) — snapshots encouraged, inline preferred
- Screen/feature components — behavioral tests only, no snapshots
- Error states and empty states — inline snapshots for static states, behavioral for transitions
- Any component with
Date.now(),Math.random(), or external IDs — either freeze time/mock randomness, or no snapshot
Summary
Snapshot testing is a sharp tool that punishes misuse. The sweet spot is small, focused, inline snapshots on presentational components — used alongside behavioral tests, not instead of them. Keep snapshots small enough to read in seconds, treat every failure as a decision point (update or fix?), and exclude snapshots from components that change frequently. Follow these rules and your snapshot suite becomes a genuine regression net rather than a bureaucratic burden.