-
-
Notifications
You must be signed in to change notification settings - Fork 0
API Reference
Complete API documentation for vitest-react-profiler.
- Core Functions
- Sync Matchers
- Snapshot API
- Async Utilities
- Async Matchers
- ProfiledComponent API
- Utility Functions
- TypeScript Types
Wraps a React component with profiling capabilities.
Signature:
function withProfiler<P>(
Component: ComponentType<P>,
displayName?: string
): ProfiledComponent<P>Parameters:
-
Component- React component to profile -
displayName(optional) - Custom name for debugging
Returns:
-
ProfiledComponent<P>- Enhanced component with profiling methods
Example:
const MyComponent = ({ title }: { title: string }) => <h1>{title}</h1>;
const ProfiledComponent = withProfiler(MyComponent, "MyComponent");
render(<ProfiledComponent title="Hello" />);
expect(ProfiledComponent).toHaveRenderedTimes(1);Combines withProfiler() and render() in one call.
Signature:
function renderProfiled<P>(
Component: ComponentType<P>,
props: P,
options?: RenderProfiledOptions
): RenderProfiledResult<P>Parameters:
-
Component- React component to profile -
props- Initial props -
options(optional):-
displayName?: string- Custom name for profiled component -
renderOptions?: RenderOptions- React Testing Library options
-
Returns:
interface RenderProfiledResult<P> {
component: ProfiledComponent<P>;
rerender: (props: Partial<P>) => void;
// All RTL utilities: container, unmount, debug, etc.
}Example:
const { component, rerender } = renderProfiled(Counter, { value: 0 });
expect(component).toHaveRenderedTimes(1);
rerender({ value: 1 }); // Partial props merge
expect(component).toHaveRenderedTimes(2);Profile a React hook to detect extra renders.
Signatures:
// Hook without parameters
function profileHook<TResult>(
hook: () => TResult
): ProfileHookResult<object, TResult>;
// Hook without parameters, with options
function profileHook<TResult>(
hook: () => TResult,
options: ProfileHookOptions
): ProfileHookResult<object, TResult>;
// Hook with parameters
function profileHook<TProps, TResult>(
hook: (props: TProps) => TResult,
initialProps: TProps
): ProfileHookResult<TProps, TResult>;
// Hook with parameters and options
function profileHook<TProps, TResult>(
hook: (props: TProps) => TResult,
initialProps: TProps,
options: ProfileHookOptions
): ProfileHookResult<TProps, TResult>;Parameters:
-
hook- The hook function to profile -
initialProps(optional) - Initial props for hooks with parameters -
options(optional):-
renderOptions?: RenderOptions- React Testing Library render options (wrapper, etc.)
-
Returns:
interface ProfileHookResult<TProps, TResult> {
result: { readonly current: TResult };
rerender: (newProps?: TProps) => void;
unmount: () => void;
ProfiledHook: ProfiledComponent<TProps>;
}Examples:
// Basic usage
const { result, ProfiledHook } = profileHook(() => useMyHook());
expect(ProfiledHook).toHaveRenderedTimes(1);
// With parameters
const { result, ProfiledHook, rerender } = profileHook(
({ value }) => useCounter(value),
{ value: 1 }
);
rerender({ value: 2 });
expect(ProfiledHook).toHaveRenderedTimes(2);
// With context provider (wrapper)
const wrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider>{children}</ThemeProvider>
);
const { result, ProfiledHook } = profileHook(
() => useTheme(),
{ renderOptions: { wrapper } }
);
expect(result.current.theme).toBe('light');
// With both parameters and wrapper
const { result, ProfiledHook } = profileHook(
(props) => useThemedCounter(props),
{ initialCount: 10 },
{ renderOptions: { wrapper: ThemeProvider } }
);Simplified API for hook profiling with built-in assertions.
Signatures:
Same as profileHook() - supports all four overloads.
Returns:
interface HookProfiler<TProps, TResult> {
result: { readonly current: TResult };
rerender: (newProps?: TProps) => void;
unmount: () => void;
ProfiledHook: ProfiledComponent<TProps>;
expectRenderCount: (expected: number) => void; // Built-in assertion
getRenderCount: () => number;
getRenderHistory: () => readonly PhaseType[];
getLastRender: () => PhaseType | undefined;
}Examples:
// Basic usage with built-in assertion
const profiler = createHookProfiler(() => useMyHook());
profiler.expectRenderCount(1);
// With context provider
const profiler = createHookProfiler(
() => useTheme(),
{ renderOptions: { wrapper: ThemeProvider } }
);
profiler.expectRenderCount(1);
expect(profiler.result.current.theme).toBe('light');Asserts that component has rendered at least once.
expect(ProfiledComponent).toHaveRendered();
expect(ProfiledComponent).not.toHaveRendered();Asserts exact number of renders.
Parameters:
-
count: number- Expected render count
expect(ProfiledComponent).toHaveRenderedTimes(3);
expect(ProfiledComponent).not.toHaveRenderedTimes(5);Error Message:
Expected 3 renders, but got 5 (1 mount, 4 updates)
#1 [mount phase]
#2 [update phase]
#3 [update phase]
#4 [update phase]
#5 [update phase]
π‘ Tip: Use Component.getRenderHistory() to inspect all render details
Asserts that component mounted exactly once.
expect(ProfiledComponent).toHaveMountedOnce();Asserts that component never mounted.
const ProfiledComponent = withProfiler(MyComponent);
// Don't render
expect(ProfiledComponent).toHaveNeverMounted();Asserts that component only updated (no mounts).
// Useful for testing child components that remount
expect(ProfiledComponent).toHaveOnlyUpdated();Asserts that component rendered with specific phase.
Parameters:
-
phase: PhaseType-"mount"|"update"|"nested-update"
expect(ProfiledComponent).toHaveRenderedWithPhase("mount");
expect(ProfiledComponent).toHaveRenderedWithPhase("update");Asserts that last render had specific phase.
rerender(<ProfiledComponent value={2} />);
expect(ProfiledComponent).toHaveLastRenderedWithPhase("update");Since: v1.8.0
Asserts that component renders within budget constraints. Provides DX convenience by checking multiple render constraints in a single assertion.
Parameters:
-
budget: RenderCountBudget- Budget constraints object-
maxRenders?: number- Maximum total renders -
maxMounts?: number- Maximum mount renders -
maxUpdates?: number- Maximum update renders (includes nested-updates) -
componentName?: string- Component name for error messages (optional)
-
Key Benefits:
- Declarative: One assertion instead of N separate asserts
- Readable: Express performance budget as a single object
- Flexible: Specify only the constraints you care about
Example:
// Traditional way (verbose):
expect(ProfiledComponent).toHaveRenderedTimes(3);
expect(ProfiledComponent).toHaveMountedOnce();
expect(ProfiledComponent.getRendersByPhase('update')).toHaveLength(2);
// With budget (declarative):
expect(ProfiledComponent).toMeetRenderCountBudget({
maxRenders: 3,
maxMounts: 1,
maxUpdates: 2,
componentName: 'Dashboard'
});
// Flexible constraints - check only what matters:
expect(ProfiledComponent).toMeetRenderCountBudget({
maxRenders: 5 // Only care about total renders
});Error Message:
Expected Dashboard to meet render count budget:
Total renders: 5 (budget: 3) β
Mounts: 1 (budget: 1) β
Updates: 4 (budget: 2) β
Violations:
Total renders exceeded: 5 > 3
Update count exceeded: 4 > 2
Actual: 5 renders (1 mount, 4 updates)
#1 [mount phase]
#2 [update phase]
#3 [update phase]
#4 [update phase]
#5 [update phase]
Use Cases:
- Performance budgets for complex components
- Regression testing for render counts
- CI/CD performance gates
- Reducing test boilerplate
See Also:
- Example:
examples/performance/RenderBudget.test.tsx - Feature analysis: docs/reports/render-count-features-analysis.md Β§ 6.1
Since: v1.8.0
β οΈ Diagnostic/Debugging Tool: This is NOT a regular test matcher. Use it to discover and diagnose render loop problems when tests hang or timeout. You don't write tests expecting infinite loopsβthis matcher helps you find them when they occur.
Asserts that component does not have suspicious render loop patterns. Detects infinite loops BEFORE hitting React's MAX_SAFE_RENDERS limit (10,000 renders).
Fills the Gap:
0 renders ββββββββββββββββββββββββββββββββββββββ 10,000 renders
β β
β [normal] [suspicious] [circuit breaker] β
β β β β β
ββββββββββ΄βββββββββββββ΄ββββββββββββββββββ΄βββββββββββ
β
notToHaveRenderLoops
detects here
vs MAX_SAFE_RENDERS:
-
MAX_SAFE_RENDERS = 10,000: Emergency stop, breaks test, doesn't help catch 5-10 extra renders -
notToHaveRenderLoops: Detects suspicious patterns (3+ consecutive updates), provides diagnostic info
Parameters:
-
options?: RenderLoopOptions- Loop detection options-
maxConsecutiveUpdates?: number- Max consecutive 'update' phases (default: 10) -
maxConsecutiveNested?: number- Max consecutive 'nested-update' phases (default: same as maxConsecutiveUpdates) -
ignoreInitialUpdates?: number- Skip first N updates (default: 0) -
showFullHistory?: boolean- Include full render history in error (default: false) -
componentName?: string- Component name for error messages (optional)
-
Key Benefits:
- Early Detection: Catches loops at 3-10 renders, not 10,000
- Diagnostic: Informative error messages with loop sequences
- Configurable: Adjust thresholds for your use case
Example:
// Default threshold (10 consecutive updates)
expect(ProfiledComponent).notToHaveRenderLoops();
// Custom threshold for stricter checking
expect(ProfiledComponent).notToHaveRenderLoops({
maxConsecutiveUpdates: 3,
componentName: 'Counter'
});
// Ignore initialization sequence
expect(ProfiledComponent).notToHaveRenderLoops({
ignoreInitialUpdates: 2, // Skip first 2 updates
maxConsecutiveUpdates: 5
});Error Message:
Expected Counter not to have render loops, but found:
Suspicious pattern: 11 consecutive 'update' phases (threshold: 10)
Loop sequence (renders #2-#12):
#2 [update phase]
#3 [update phase]
#4 [update phase]
#5 [update phase]
#6 [update phase]
#7 [update phase]
#8 [update phase]
#9 [update phase]
#10 [update phase]
#11 [update phase]
... and 1 more
Potential causes:
- useEffect with missing/incorrect dependencies
- setState called during render
- Circular state updates between components
π‘ Tip: Use Counter.getRenderHistory() to inspect full history
When to Use (Diagnostic Scenarios):
-
Test hangs during development
- Your test freezes/hangs β Add
notToHaveRenderLoops()β Get diagnostic message showing the loop - Example:
useEffectwithout dependencies causing infinite updates
- Your test freezes/hangs β Add
-
CI timeout without clear error
- Test times out in CI with no useful message β Add this matcher β Get informative error instead of timeout
- Helps identify the exact render sequence causing the problem
-
Debugging complex component performance
- Suspect unnecessary re-renders in production β Add temporarily to tests β Identify cascade update chains
- Remove after fixing the issue
-
Code review / PR validation
- Reviewer suspects potential loop β Add matcher to verify β Proves component is safe
When NOT to Use:
- β Don't add to every test "just in case"
- β Don't use as a regular assertion like
toHaveRenderedTimes() - β Not a replacement for proper
useEffectdependency arrays
This matcher is a diagnostic tool for problem discovery, not a standard testing practice.
Common Patterns Detected:
// β BAD: useEffect without dependencies
useEffect(() => {
if (count < 100) {
setCount(c => c + 1); // Infinite loop!
}
}); // Missing [count] dependency
// β BAD: Cascade updates
useEffect(() => {
if (step1 === 1) setStep2(1);
}, [step1]);
useEffect(() => {
if (step2 === 1) setStep3(1);
}, [step2]);Real-World Diagnostic Workflow:
// Step 1: Test hangs - you don't know why
it('should update counter', () => {
render(<Counter />);
// Test hangs here... β
});
// Step 2: Add diagnostic matcher
it('should update counter', () => {
const Profiled = withProfiler(Counter);
render(<Profiled />);
// Add this to diagnose the hang
expect(Profiled).notToHaveRenderLoops({
maxConsecutiveUpdates: 5
});
// Now you get: "Suspicious pattern: 11 consecutive 'update' phases"
// β Points to useEffect infinite loop
});
// Step 3: Fix the bug
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(c => c + 1);
}, []); // β
Added dependency array - loop fixed!
return <div>{count}</div>;
}
// Step 4: Remove diagnostic matcher (optional)
// Once bug is fixed, you can remove the matcher
// or keep it as a regression guardSee Also:
- Example:
examples/hooks/RenderLoops.test.tsx - Feature analysis: docs/reports/render-count-features-analysis.md Β§ 6.2
Since: v1.10.0 | Extended: v1.11.0
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.
π See Snapshot API for full documentation.
Quick reference:
| Method/Matcher | Description |
|---|---|
snapshot() |
Create baseline for render counting |
getRendersSinceSnapshot() |
Get renders since baseline |
toHaveRerenderedOnce() |
Assert exactly 1 rerender |
toNotHaveRerendered() |
Assert no rerenders |
toHaveRerendered() |
Assert at least 1 rerender (v1.11.0) |
toHaveRerendered(n) |
Assert exact N rerenders (v1.11.0) |
toEventuallyRerender() |
Wait for async rerender (v1.11.0) |
toEventuallyRerenderTimes(n) |
Wait for exact N async rerenders (v1.11.0) |
Basic example:
const ProfiledComponent = withProfiler(Counter);
render(<ProfiledComponent />);
ProfiledComponent.snapshot();
fireEvent.click(screen.getByText('Increment'));
expect(ProfiledComponent).toHaveRerenderedOnce();Event-based utilities for waiting on renders (no polling overhead).
Wait for exact render count.
Signature:
function waitForRenders<P>(
component: ProfiledComponent<P>,
count: number,
options?: WaitOptions
): Promise<void>Parameters:
-
component- Profiled component -
count- Expected render count -
options.timeout?- Max wait time in ms (default: 1000)
Example:
const AsyncComponent = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => setCount(1), 100);
}, []);
return <div>{count}</div>;
};
const Profiled = withProfiler(AsyncComponent);
render(<Profiled />);
// Wait for mount + async update
await waitForRenders(Profiled, 2);
expect(Profiled.getRenderCount()).toBe(2);Wait for at least N renders.
// Wait for at least 2 renders
await waitForMinimumRenders(ProfiledComponent, 2);
expect(ProfiledComponent.getRenderCount()).toBeGreaterThanOrEqual(2);Wait for specific render phase.
Parameters:
-
phase: PhaseType-"mount"|"update"|"nested-update"
const { rerender } = render(<ProfiledComponent />);
rerender(<ProfiledComponent value={2} />);
await waitForPhase(ProfiledComponent, "update");
expect(ProfiledComponent.getRendersByPhase("update").length).toBeGreaterThan(0);Since: v1.12.0
Wait for component to "stabilize" - when renders stop for a specified debounce period. Uses debounce pattern: resolves after debounceMs with no renders.
Signature:
function waitForStabilization(options?: StabilizationOptions): Promise<StabilizationResult>
interface StabilizationOptions {
debounceMs?: number; // Default: 50 - Time without renders to consider stable
timeout?: number; // Default: 1000 - Max wait time before timeout error
}
interface StabilizationResult {
renderCount: number; // Number of renders tracked during wait
lastPhase?: PhaseType; // Last render phase (undefined if no renders)
}Use Cases:
- Virtualized lists - Wait for scroll to complete and list to stabilize
- Debounced search - Wait for search input to settle
- Animations - Wait for animation frames to complete
- Data loading cascades - Wait for dependent data fetches to resolve
Example:
const VirtualList = ({ items }) => {
// Complex virtualization with many renders during scroll
return <div>{items.map(item => <Item key={item.id} {...item} />)}</div>;
};
const ProfiledList = withProfiler(VirtualList);
const { rerender } = render(<ProfiledList items={items} />);
// Start waiting for stabilization
const promise = ProfiledList.waitForStabilization({
debounceMs: 50, // Wait 50ms after last render
timeout: 2000 // Fail after 2s if still rendering
});
// Simulate rapid scroll (many rerenders)
for (let i = 0; i < 10; i++) {
rerender(<ProfiledList items={items} scrollTop={i * 100} />);
}
// Wait for list to stabilize
const result = await promise;
console.log(`List stabilized after ${result.renderCount} renders`);
expect(result.lastPhase).toBe("update");Error Handling:
// Timeout error when component never stabilizes
try {
await ProfiledComponent.waitForStabilization({ timeout: 100 });
} catch (error) {
// "StabilizationTimeoutError: Component did not stabilize within 100ms (15 renders)"
}
// Validation error when debounceMs >= timeout
await ProfiledComponent.waitForStabilization({
debounceMs: 200,
timeout: 100 // Error: debounceMs must be less than timeout
});Important: Start waiting BEFORE triggering renders (same pattern as waitForNextRender):
// β
Correct: Start waiting first
const promise = ProfiledComponent.waitForStabilization();
triggerManyRenders();
await promise;
// β Wrong: Waiting after renders - may resolve immediately
triggerManyRenders();
await ProfiledComponent.waitForStabilization(); // May miss renders!Event-based matchers that wait for conditions.
Assert component eventually renders exact number of times.
// Default 1000ms timeout
await expect(ProfiledComponent).toEventuallyRenderTimes(3);
// Custom timeout
await expect(ProfiledComponent).toEventuallyRenderTimes(5, { timeout: 2000 });Assert component eventually renders at least N times.
await expect(ProfiledComponent).toEventuallyRenderAtLeast(2);Assert component eventually reaches specific phase.
await expect(ProfiledComponent).toEventuallyReachPhase("update");
await expect(ProfiledComponent).toEventuallyReachPhase("mount", { timeout: 500 });Since: v1.12.0
Assert component eventually stabilizes (stops rendering for debounceMs period).
Signature:
interface StabilizationOptions {
debounceMs?: number; // Default: 50
timeout?: number; // Default: 1000
}Example:
const SearchResults = ({ query }) => {
const [results, setResults] = useState([]);
useEffect(() => {
// Debounced API call triggers multiple renders
debouncedSearch(query).then(setResults);
}, [query]);
return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>;
};
const ProfiledSearch = withProfiler(SearchResults);
render(<ProfiledSearch query="react" />);
// Wait for search to stabilize
await expect(ProfiledSearch).toEventuallyStabilize({
debounceMs: 100,
timeout: 2000
});
// Component is now stable - verify results
expect(screen.getAllByRole('listitem')).toHaveLength(10);Validation Errors:
The matcher returns pass: false with helpful messages for invalid options:
// debounceMs must be positive
await expect(ProfiledComponent).toEventuallyStabilize({ debounceMs: 0 });
// "Expected debounceMs to be a positive number, received 0"
// debounceMs must be less than timeout
await expect(ProfiledComponent).toEventuallyStabilize({
debounceMs: 100,
timeout: 50
});
// "Expected debounceMs (100) to be less than timeout (50)"See Also:
- waitForStabilization - Lower-level API with result details
Methods available on profiled components.
Returns total number of renders.
const count = ProfiledComponent.getRenderCount();Returns array of all render phases (frozen).
const history = ProfiledComponent.getRenderHistory();
// ["mount", "update", "update"]Returns phase of last render.
const lastPhase = ProfiledComponent.getLastRender();
// "update"Returns phase at specific index (0-based).
const firstRender = ProfiledComponent.getRenderAt(0);
// "mount"Returns all renders matching specific phase.
const updates = ProfiledComponent.getRendersByPhase("update");
// ["update", "update", "update"]Returns true if component has mounted.
if (ProfiledComponent.hasMounted()) {
// Component mounted
}Subscribe to render events (real-time notifications).
Signature:
function onRender(
callback: (info: RenderEventInfo) => void
): () => voidParameters:
-
callback- Function called on each render-
info.count- Total render count -
info.phase- Current phase -
info.history- All render phases (read-only)
-
Returns:
- Unsubscribe function
Example:
const renders: RenderEventInfo[] = [];
const unsubscribe = ProfiledComponent.onRender((info) => {
renders.push(info);
console.log(`Render #${info.count}: ${info.phase}`);
});
// Trigger renders...
unsubscribe(); // CleanupWait for next component render.
Important: Create promise BEFORE triggering action!
// β
Correct: Start waiting first
const promise = ProfiledComponent.waitForNextRender({ timeout: 1000 });
// Then trigger action
fireEvent.click(button);
// Then await
const info = await promise;
expect(info.phase).toBe("update");// β Wrong: Action before waiting
fireEvent.click(button);
const info = await ProfiledComponent.waitForNextRender(); // May miss render!Since: v1.10.0
Creates a baseline for render counting. See Snapshot API for full documentation.
ProfiledComponent.snapshot();Since: v1.10.0
Returns the number of renders since the last snapshot() call. See Snapshot API for full documentation.
const delta = ProfiledComponent.getRendersSinceSnapshot();Manually clears the component registry. This function is exported for advanced use cases where automatic cleanup needs to be controlled manually.
Signature:
function clearRegistry(): voidImport:
import { clearRegistry } from 'vitest-react-profiler';Usage:
import { clearRegistry } from 'vitest-react-profiler';
// Disable auto-setup and use manual cleanup
describe('Manual cleanup tests', () => {
afterEach(() => {
clearRegistry(); // Manual cleanup
});
});When to use:
- β NOT needed in 99% of cases - automatic cleanup via
auto-setup.tshandles this - β Advanced scenarios with custom test lifecycle
- β Integration with non-standard test runners
- β Debugging registry state
Important Notes:
- The library automatically cleans up between tests using Vitest's
afterEach()andafterAll()hooks - Manual cleanup is rarely needed - only use when you have specific requirements
- See Best Practices for more information on automatic cleanup
Related:
interface ProfiledComponent<P> extends ComponentType<P> {
getRenderCount(): number;
getRenderHistory(): readonly PhaseType[];
getLastRender(): PhaseType | undefined;
getRenderAt(index: number): PhaseType | undefined;
getRendersByPhase(phase: PhaseType): readonly PhaseType[];
hasMounted(): boolean;
onRender(callback: (info: RenderEventInfo) => void): () => void;
waitForNextRender(options?: WaitOptions): Promise<RenderEventInfo>;
snapshot(): void; // Since v1.10.0
getRendersSinceSnapshot(): number; // Since v1.10.0
readonly OriginalComponent: ComponentType<P>;
}interface RenderEventInfo {
count: number; // Total render count
phase: PhaseType; // "mount" | "update"
readonly history: readonly PhaseType[]; // All phases
}type PhaseType = "mount" | "update" | "nested-update";interface WaitOptions {
timeout?: number; // Max wait time in ms (default: 1000)
}interface ProfileHookOptions {
/**
* React Testing Library render options.
* Most commonly used for wrapping the hook in a context provider.
*/
renderOptions?: RenderOptions;
}Example:
const options: ProfileHookOptions = {
renderOptions: {
wrapper: ({ children }) => <MyProvider>{children}</MyProvider>
}
};
const { result } = profileHook(() => useMyHook(), options);- Examples - Real-world usage
- Hook Profiling - Testing hooks
- Best Practices - Recommendations