The Developer's Quarterly · Testing ToolsVol. III · March 2026

Custom Serializers for Snapshot Testing in React:Keep Snapshots Stable Without Hiding Real Changes

Custom serializers trim unstable noise from React snapshots so code review stays focused on meaningful structure instead of generated details.

Snapshot testing is useful when you want to notice structural changes quickly, but raw snapshots can become noisy fast. Custom serializers help by stripping away unstable details before the snapshot is written, so the test keeps the signal and drops the churn.

That matters in React because component output often includes values that are correct but not stable: generated class names, inline styles, timestamps, random IDs, or framework-specific wrappers. A serializer gives you one place to normalize that output instead of repeating cleanup logic in every test.

The short version

If your snapshots keep changing for reasons that are not meaningful, add a serializer. If the snapshot should show the important structure but not implementation noise, a serializer is usually the right fix.

The goal is not to hide everything. The goal is to make the snapshot easier to review.

What a serializer does

In Jest, a custom serializer receives a value before it is printed into the snapshot. You can inspect that value, reshape it, or replace unstable fields with stable text.

For React tests, that often means simplifying component trees, masking dynamic IDs, or normalizing generated markup.

Custom serializer
const stripTestIds = { test(value: unknown) { return typeof value === 'object' && value !== null; }, print(value: Record<string, unknown>, serialize: (value: unknown) => string) { const clone = { ...value }; if ('data-testid' in clone) { clone['data-testid'] = '[removed]'; } return serialize(clone); }, };

That example is intentionally small. The important part is the pattern: detect the thing you want to normalize, then return a stable representation.

A common React problem

Here is a simple component that produces output with a generated ID.

Field component
type FieldProps = { label: string; id: string; }; export function Field({ label, id }: FieldProps) { return ( <div> <label htmlFor={id}>{label}</label> <input id={id} data-testid={'field-' + id} /> </div> ); }

Without a serializer, the snapshot might include the full data-testid value or some other generated detail that changes from test to test. That makes code review slower because every snapshot diff looks like a behavioral change even when it is not.

Registering the serializer

The usual Jest setup is straightforward.

Register in setup
import { stripTestIds } from './serializers/stripTestIds'; expect.addSnapshotSerializer(stripTestIds);

Once that is registered, any snapshot written in that test file can pass through the serializer first.

If you prefer a global setup file, that works too. The point is to keep the normalization close to the snapshot system, not scattered across multiple tests.

Before and after

The difference is easy to see when you compare a raw snapshot with a normalized one.

Snapshot test
test('renders the field', () => { const { container } = render(<Field label="Email" id="email-42" />); expect(container.firstChild).toMatchSnapshot(); });

With a serializer in place, the stored snapshot can focus on the structure that matters:

Normalized snapshot
<div> <label htmlFor="email-42">Email</label> <input data-testid="[removed]" id="email-42" /> </div>

That is still enough to tell you whether the component renders the right label and input relationship. It is just less fragile.

Good uses for serializers

1 Hide unstable IDs

Normalize generated IDs, test IDs, and random suffixes that are not part of the user-facing behavior.

2 Simplify wrapper noise

Remove framework wrappers or helper markup that makes the snapshot harder to read than it needs to be.

3 Normalize repeated markup

Reduce large repeated structures to the stable shape that the reviewer actually needs to inspect.

A practical rule

Custom serializers are best when the output is still meaningful after the cleanup. If the serializer has to transform almost everything, the snapshot is probably trying to test the wrong thing.

That is the boundary to watch:

Keep behavior in assertions Use regular expectations for logic and interaction.
Keep structure in snapshots Let snapshots show the shape of the rendered output.
Keep noise out with serializers Normalize unstable details before the snapshot is stored.
Keep the scope narrow Only serialize what makes the diff noisy, not what makes the test meaningful.

When not to use one

If the only reason a snapshot is noisy is that the component is too large, a serializer is not the fix. Split the component or stop snapshotting the whole tree.

If the serializer starts hiding real behavior, remove it. A bad serializer can make a broken UI look stable.

Bottom line

Custom serializers are a small but effective way to keep React snapshots readable. They normalize the unstable parts of the output so the snapshot shows what actually matters.

Use them when the diff is noisy, not when the test is overreaching. That keeps snapshots useful instead of ritualistic.