The Snapshot API provides methods for creating render baselines and measuring render deltas. This is the primary tool for testing React optimizations like `React.memo`, `useCallback`, and ensuring single renders per user action.
## Table of Contents
- [Overview](#overview)
- [Core Methods](#core-methods)
- [snapshot()](#snapshot)
- [getRendersSinceSnapshot()](#getrenderssincenapshot)
- [Sync Matchers](#sync-matchers)
- [toHaveRerenderedOnce()](#tohavererenderedonce)
- [toNotHaveRerendered()](#tonothavererendered)
- [toHaveRerendered()](#tohavererendered) (v1.11.0)
- [toHaveRerendered(n)](#tohaverenderedn) (v1.11.0)
- [Async Matchers](#async-matchers) (v1.11.0)
- [toEventuallyRerender()](#toeventuallyrerender)
- [toEventuallyRerenderTimes(n)](#toeventuallyrerendertimesn)
- [Use Cases](#use-cases)
- [Examples](#examples)
---
## Overview
**Since:** v1.10.0 | **Extended:** v1.11.0
The Snapshot API enables delta-based render testing. Instead of counting total renders, you create a baseline (snapshot) and then measure how many renders occurred after that point.
**Key concepts:**
1. **Snapshot** - Creates a baseline at the current render count
2. **Delta** - The number of renders since the last snapshot
3. **Matchers** - Assertions that use the delta for testing
**When to use:**
- Testing `React.memo` effectiveness
- Verifying `useCallback` stability
- Ensuring single render per user action
- Performance budget testing
---
## Core Methods
### `snapshot()`
Creates a baseline for render counting. All subsequent `getRendersSinceSnapshot()` calls return the number of renders since this baseline.
**Signature:**
```typescript
function snapshot(): void
```
**Example:**
```typescript
const ProfiledComponent = withProfiler(MyComponent);
render();
// Before snapshot: includes initial mount
expect(ProfiledComponent.getRendersSinceSnapshot()).toBe(1);
ProfiledComponent.snapshot(); // Create baseline
// Immediately after snapshot: delta is 0
expect(ProfiledComponent.getRendersSinceSnapshot()).toBe(0);
```
---
### `getRendersSinceSnapshot()`
Returns the number of renders since the last `snapshot()` call.
**Signature:**
```typescript
function getRendersSinceSnapshot(): number
```
**Example:**
```typescript
const ProfiledComponent = withProfiler(Counter);
const { rerender } = render();
ProfiledComponent.snapshot(); // Baseline
rerender();
expect(ProfiledComponent.getRendersSinceSnapshot()).toBe(1);
rerender();
expect(ProfiledComponent.getRendersSinceSnapshot()).toBe(2);
// Take new snapshot to reset baseline
ProfiledComponent.snapshot();
expect(ProfiledComponent.getRendersSinceSnapshot()).toBe(0);
```
---
## Sync Matchers
### `toHaveRerenderedOnce()`
**Since:** v1.10.0
Asserts that exactly one rerender occurred since the last `snapshot()` call. Perfect for verifying single render per user action.
**Example:**
```typescript
const ProfiledComponent = withProfiler(Counter);
render();
ProfiledComponent.snapshot();
fireEvent.click(screen.getByText('Increment'));
// Single state change = single rerender
expect(ProfiledComponent).toHaveRerenderedOnce();
```
**Error Message:**
```
Expected component to rerender once after snapshot, but it rerendered 3 times
#1 [mount phase]
#2 [update phase] <- snapshot here
#3 [update phase]
#4 [update phase]
#5 [update phase]
```
---
### `toNotHaveRerendered()`
**Since:** v1.10.0
Asserts that no rerenders occurred since the last `snapshot()` call. Perfect for testing `React.memo` effectiveness and optimization validations.
**Example:**
```typescript
const ProfiledComponent = withProfiler(MemoizedComponent);
const { rerender } = render();
ProfiledComponent.snapshot();
// Change unrelated prop - memo should prevent rerender
rerender();
expect(ProfiledComponent).toNotHaveRerendered();
```
**Error Message:**
```
Expected component not to rerender after snapshot, but it rerendered 1 time
#1 [mount phase]
#2 [update phase]
```
---
### `toHaveRerendered()`
**Since:** v1.11.0
Asserts that component rerendered at least once after snapshot. Use this when you want to verify that an action triggered a rerender, regardless of how many times.
**Signature:**
```typescript
expect(component).toHaveRerendered();
expect(component).not.toHaveRerendered();
```
**Example:**
```typescript
const ProfiledComponent = withProfiler(Counter);
render();
ProfiledComponent.snapshot();
fireEvent.click(screen.getByText('Increment'));
// Verify rerender happened (at least 1)
expect(ProfiledComponent).toHaveRerendered();
```
**With `.not` modifier:**
```typescript
ProfiledComponent.snapshot();
// No action taken - verify no rerender
expect(ProfiledComponent).not.toHaveRerendered();
```
**Error Message (pass case with `.not`):**
```
Expected component not to rerender after snapshot, but it rerendered 1 time
```
**Error Message (fail case):**
```
Expected component to rerender after snapshot, but it did not
```
---
### `toHaveRerendered(n)`
**Since:** v1.11.0
Asserts that component rerendered exactly `n` times after snapshot. Use this to verify exact render counts for detecting double-render bugs or validating performance budgets.
**Signature:**
```typescript
expect(component).toHaveRerendered(n: number);
```
**Parameters:**
- `n: number` - Expected exact number of rerenders (non-negative integer)
**Example:**
```typescript
const ProfiledComponent = withProfiler(Counter);
render();
ProfiledComponent.snapshot();
// 3 clicks = 3 state updates = 3 rerenders
fireEvent.click(screen.getByText('Increment'));
fireEvent.click(screen.getByText('Increment'));
fireEvent.click(screen.getByText('Increment'));
expect(ProfiledComponent).toHaveRerendered(3);
```
**Detecting double-render bugs:**
```typescript
ProfiledComponent.snapshot();
fireEvent.click(screen.getByText('Submit'));
// If this fails with n > 1, you have a double-render bug!
expect(ProfiledComponent).toHaveRerendered(1);
```
**Zero rerenders:**
```typescript
ProfiledComponent.snapshot();
// Alternative to toNotHaveRerendered()
expect(ProfiledComponent).toHaveRerendered(0);
```
**With `.not` modifier:**
```typescript
ProfiledComponent.snapshot();
fireEvent.click(screen.getByText('Increment'));
fireEvent.click(screen.getByText('Increment'));
// 2 rerenders - NOT 1, NOT 3
expect(ProfiledComponent).not.toHaveRerendered(1);
expect(ProfiledComponent).not.toHaveRerendered(3);
expect(ProfiledComponent).toHaveRerendered(2);
```
**Error Message:**
```
Expected component to rerender 3 times after snapshot, but it rerendered 1 time
#1 [mount phase]
#2 [update phase]
💡 Tip: Use Component.getRenderHistory() to inspect all render details
```
**Validation:**
The matcher validates the `n` parameter and throws if invalid:
```typescript
expect(ProfiledComponent).toHaveRerendered(-1); // Error: Must be non-negative
expect(ProfiledComponent).toHaveRerendered(1.5); // Error: Must be integer
expect(ProfiledComponent).toHaveRerendered("3"); // Error: Must be number
```
---
## Async Matchers
**Since:** v1.11.0
Async matchers wait for rerenders to occur. They use event-based approach with `onRender()` for instant notification - no polling overhead.
### `toEventuallyRerender()`
Waits for at least one rerender after snapshot within the timeout period. Resolves immediately if component already rerendered.
**Signature:**
```typescript
await expect(component).toEventuallyRerender(options?: WaitOptions);
```
**Parameters:**
- `options.timeout?: number` - Maximum wait time in ms (default: 1000)
**Example:**
```typescript
const ProfiledComponent = withProfiler(AsyncCounter);
render();
ProfiledComponent.snapshot();
// Trigger async action (setTimeout, fetch, etc.)
fireEvent.click(screen.getByText('Async Increment'));
// Wait for rerender (default 1000ms timeout)
await expect(ProfiledComponent).toEventuallyRerender();
// Verify state updated
expect(screen.getByTestId('count')).toHaveTextContent('1');
```
**Custom timeout:**
```typescript
await expect(ProfiledComponent).toEventuallyRerender({ timeout: 5000 });
```
**Immediate resolution:**
If component already rerendered before `await`, resolves immediately:
```typescript
ProfiledComponent.snapshot();
fireEvent.click(screen.getByText('Sync Increment')); // Sync update
// Resolves instantly - no waiting
await expect(ProfiledComponent).toEventuallyRerender();
```
**With `.not` modifier:**
```typescript
ProfiledComponent.snapshot();
// Assert component does NOT rerender within timeout
await expect(ProfiledComponent).not.toEventuallyRerender({ timeout: 100 });
```
**Error Message (timeout):**
```
Expected component to rerender after snapshot within 1000ms, but it did not
#1 [mount phase]
💡 Tip: Use Component.getRenderHistory() to inspect all render details
```
---
### `toEventuallyRerenderTimes(n)`
Waits for exact number of rerenders after snapshot. Fails early if count exceeded (doesn't wait for timeout).
**Signature:**
```typescript
await expect(component).toEventuallyRerenderTimes(n: number, options?: WaitOptions);
```
**Parameters:**
- `n: number` - Expected exact number of rerenders
- `options.timeout?: number` - Maximum wait time in ms (default: 1000)
**Example:**
```typescript
const ProfiledComponent = withProfiler(AsyncCounter);
render();
ProfiledComponent.snapshot();
// Trigger action that causes 3 sequential async updates
fireEvent.click(screen.getByText('Async +3'));
// Wait for exactly 3 rerenders
await expect(ProfiledComponent).toEventuallyRerenderTimes(3, { timeout: 500 });
expect(screen.getByTestId('count')).toHaveTextContent('3');
```
**Immediate resolution when count already met:**
```typescript
ProfiledComponent.snapshot();
// Sync updates
fireEvent.click(screen.getByText('+1'));
fireEvent.click(screen.getByText('+1'));
// Resolves instantly - count already met
await expect(ProfiledComponent).toEventuallyRerenderTimes(2);
```
**Early failure when count exceeded:**
```typescript
ProfiledComponent.snapshot();
// Create 5 rerenders
for (let i = 0; i < 5; i++) {
fireEvent.click(screen.getByText('+1'));
}
// Fails IMMEDIATELY - doesn't wait for timeout
await expect(ProfiledComponent).toEventuallyRerenderTimes(2);
// Error: Expected 2, but already got 5 (exceeded)
```
**Zero rerenders:**
```typescript
ProfiledComponent.snapshot();
// Assert no rerenders (immediate success if count is 0)
await expect(ProfiledComponent).toEventuallyRerenderTimes(0);
```
**Error Message (timeout):**
```
Expected component to rerender 3 times after snapshot within 1000ms, but got 1 time
#1 [mount phase]
#2 [update phase]
💡 Tip: Use Component.getRenderHistory() to inspect all render details
```
**Error Message (exceeded):**
```
Expected component to rerender 2 times after snapshot, but already got 5 times (exceeded)
#1 [mount phase]
#2 [update phase]
#3 [update phase]
#4 [update phase]
#5 [update phase]
#6 [update phase]
```
---
## Use Cases
### 1. Testing Single Render Per Action
```typescript
it('should render once per click', () => {
const ProfiledCounter = withProfiler(Counter);
render();
ProfiledCounter.snapshot();
fireEvent.click(screen.getByText('Increment'));
expect(ProfiledCounter).toHaveRerenderedOnce();
});
```
### 2. Testing React.memo Effectiveness
```typescript
it('should not rerender when unrelated props change', () => {
const ProfiledList = withProfiler(MemoizedList);
const items = [1, 2, 3];
const { rerender } = render();
ProfiledList.snapshot();
rerender();
// memo prevents rerender since items didn't change
expect(ProfiledList).toNotHaveRerendered();
});
```
### 3. Testing useCallback Stability
```typescript
it('should not rerender child when parent state changes', () => {
// Child receives stable callback via useCallback
ProfiledChild.snapshot();
fireEvent.click(screen.getByText('Update Parent'));
expect(ProfiledChild).toNotHaveRerendered();
});
```
### 4. Performance Budget Testing
```typescript
it('should stay within render budget', () => {
const ProfiledDashboard = withProfiler(Dashboard);
render();
ProfiledDashboard.snapshot();
performComplexOperation();
// Exact count check
expect(ProfiledDashboard).toHaveRerendered(3);
// Or flexible check
const delta = ProfiledDashboard.getRendersSinceSnapshot();
expect(delta).toBeLessThanOrEqual(5);
});
```
### 5. Testing Async State Updates
```typescript
it('should handle async data loading', async () => {
const ProfiledDataFetcher = withProfiler(DataFetcher);
render();
ProfiledDataFetcher.snapshot();
// Trigger data fetch
fireEvent.click(screen.getByText('Load Data'));
// Wait for loading + success state updates
await expect(ProfiledDataFetcher).toEventuallyRerenderTimes(2, {
timeout: 3000,
});
expect(screen.getByTestId('data')).toBeInTheDocument();
});
```
### 6. Iterative Optimization Testing
```typescript
it('should test multiple optimization scenarios', () => {
const { rerender } = render();
// Scenario 1: Same data reference
ProfiledComponent.snapshot();
rerender();
expect(ProfiledComponent).toNotHaveRerendered();
// Scenario 2: New data reference
ProfiledComponent.snapshot();
rerender();
expect(ProfiledComponent).toHaveRerendered(1);
// Scenario 3: Multiple updates
ProfiledComponent.snapshot();
rerender();
rerender();
rerender();
expect(ProfiledComponent).toHaveRerendered(3);
});
```
---
## Examples
See the example files for comprehensive usage patterns:
- **[SnapshotBasics.test.tsx](../../examples/snapshot/SnapshotBasics.test.tsx)** - Core Snapshot API usage (v1.10.0)
- **[OptimizationTesting.test.tsx](../../examples/snapshot/OptimizationTesting.test.tsx)** - Testing React optimizations
- **[ExtendedMatchers.test.tsx](../../examples/snapshot/ExtendedMatchers.test.tsx)** - Extended matchers (v1.11.0)
---
## See Also
- [API Reference](API-Reference) - Full API documentation
- [Best Practices](Best-Practices) - Testing recommendations
- [Hook Profiling](Hook-Profiling) - Testing custom hooks