-
-
Notifications
You must be signed in to change notification settings - Fork 0
Examples
Oleg edited this page Dec 18, 2025
·
4 revisions
Real-world usage patterns and scenarios.
- Basic Examples
- Snapshot Testing
- Extended Matchers (v1.11.0)
- Testing Memoization
- Async Testing
- Real-Time Tracking
- Complex Scenarios
import { render } from '@testing-library/react';
import { withProfiler } from 'vitest-react-profiler';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
};
it('should render once on mount', () => {
const ProfiledCounter = withProfiler(Counter);
render(<ProfiledCounter />);
expect(ProfiledCounter).toHaveRenderedTimes(1);
expect(ProfiledCounter).toHaveMountedOnce();
});it('should distinguish mount and update phases', () => {
const ProfiledCounter = withProfiler(Counter);
const { rerender } = render(<ProfiledCounter value={0} />);
// First render is mount
expect(ProfiledCounter).toHaveRenderedWithPhase("mount");
rerender(<ProfiledCounter value={1} />);
// Second render is update
expect(ProfiledCounter).toHaveLastRenderedWithPhase("update");
});Since: v1.10.0
The Snapshot API helps test optimization patterns by creating render baselines.
it('should render once per click', () => {
const ProfiledCounter = withProfiler(Counter);
render(<ProfiledCounter />);
// Create baseline after mount
ProfiledCounter.snapshot();
// User action
fireEvent.click(screen.getByText('Increment'));
// Verify single rerender
expect(ProfiledCounter).toHaveRerenderedOnce();
});it('should not rerender when props are equal', () => {
const ProfiledList = withProfiler(MemoizedList);
const items = [1, 2, 3];
const { rerender } = render(<ProfiledList items={items} />);
ProfiledList.snapshot();
// Same reference - memo should prevent rerender
rerender(<ProfiledList items={items} />);
expect(ProfiledList).toNotHaveRerendered();
});it('should track renders through multiple operations', () => {
const ProfiledForm = withProfiler(Form);
render(<ProfiledForm />);
// Verify mount
expect(ProfiledForm).toHaveLastRenderedWithPhase("mount");
// Test first interaction
ProfiledForm.snapshot();
fireEvent.change(input, { target: { value: 'a' } });
expect(ProfiledForm).toHaveRerenderedOnce();
// Test second interaction
ProfiledForm.snapshot();
fireEvent.change(input, { target: { value: 'ab' } });
expect(ProfiledForm).toHaveRerenderedOnce();
// Verify total renders
expect(ProfiledForm.getRenderCount()).toBe(3); // mount + 2 updates
});it('should stay within render budget', () => {
const ProfiledDashboard = withProfiler(Dashboard);
render(<ProfiledDashboard />);
ProfiledDashboard.snapshot();
// Complex operation
loadAllWidgets();
// Verify render count is within budget
const delta = ProfiledDashboard.getRendersSinceSnapshot();
expect(delta).toBeLessThanOrEqual(5);
});Since: v1.11.0
Extended matchers provide more flexible assertions for snapshot-based testing. See Snapshot API for full documentation.
it('should verify component rerendered', () => {
const ProfiledCounter = withProfiler(Counter);
render(<ProfiledCounter />);
ProfiledCounter.snapshot();
fireEvent.click(screen.getByText('Increment'));
// Verify at least one rerender happened
expect(ProfiledCounter).toHaveRerendered();
});it('should verify exact rerender count', () => {
const ProfiledCounter = withProfiler(Counter);
render(<ProfiledCounter />);
ProfiledCounter.snapshot();
// 3 clicks = 3 state updates = 3 rerenders
fireEvent.click(screen.getByText('Increment'));
fireEvent.click(screen.getByText('Increment'));
fireEvent.click(screen.getByText('Increment'));
expect(ProfiledCounter).toHaveRerendered(3);
});it('should wait for async rerender', async () => {
const ProfiledComponent = withProfiler(AsyncComponent);
render(<ProfiledComponent />);
ProfiledComponent.snapshot();
// Trigger async action
fireEvent.click(screen.getByText('Load Data'));
// Wait for rerender (default 1000ms timeout)
await expect(ProfiledComponent).toEventuallyRerender();
});it('should wait for exact rerender count', async () => {
const ProfiledComponent = withProfiler(DataFetcher);
render(<ProfiledComponent />);
ProfiledComponent.snapshot();
// Trigger action that causes multiple async updates
fireEvent.click(screen.getByText('Fetch All'));
// Wait for exactly 3 rerenders (loading + data + complete)
await expect(ProfiledComponent).toEventuallyRerenderTimes(3, {
timeout: 2000,
});
});For comprehensive examples, see:
- ExtendedMatchers.test.tsx - 24 tests covering all extended matchers
import { memo } from 'react';
const ExpensiveComponent = memo(({ data }) => {
return <ComplexVisualization data={data} />;
});
it('should skip re-renders with memo', () => {
const ProfiledComponent = withProfiler(ExpensiveComponent);
// β
Important: Wrap ProfiledComponent in memo
const MemoProfiled = memo(ProfiledComponent);
const { rerender } = render(<MemoProfiled data={data} />);
// Same props - should not re-render
rerender(<MemoProfiled data={data} />);
expect(ProfiledComponent).toHaveRenderedTimes(1);
// Different props - should re-render
rerender(<MemoProfiled data={newData} />);
expect(ProfiledComponent).toHaveRenderedTimes(2);
});Why the extra memo wrapper? React Profiler always triggers even when memo prevents re-render. The additional wrapper ensures accurate testing.
const ExpensiveCalc = ({ items }: { items: number[] }) => {
const sum = useMemo(() => {
return items.reduce((a, b) => a + b, 0);
}, [items]);
return <div>{sum}</div>;
};
it('should not re-render with same items reference', () => {
const ProfiledComponent = withProfiler(ExpensiveCalc);
const items = [1, 2, 3];
const { rerender } = render(<ProfiledComponent items={items} />);
// Same reference - should not recalculate
rerender(<ProfiledComponent items={items} />);
expect(ProfiledComponent).toHaveRenderedTimes(2);
// New reference - should recalculate
rerender(<ProfiledComponent items={[...items]} />);
expect(ProfiledComponent).toHaveRenderedTimes(3);
});const AsyncComponent = () => {
const [data, setData] = useState(null);
useEffect(() => {
setTimeout(() => {
setData({ result: "success" });
}, 100);
}, []);
return <div>{data?.result || "Loading..."}</div>;
};
it('should wait for async state update', async () => {
const Profiled = withProfiler(AsyncComponent);
render(<Profiled />);
// Wait for mount + async update
await expect(Profiled).toEventuallyRenderTimes(2);
expect(Profiled).toHaveMountedOnce();
});it('should handle multiple async updates', async () => {
const Profiled = withProfiler(AsyncComponent);
render(<Profiled />);
// Wait for exact count
await waitForRenders(Profiled, 3, { timeout: 2000 });
expect(Profiled.getRenderCount()).toBe(3);
});it('should track renders with subscription', () => {
const ProfiledCounter = withProfiler(Counter);
const { getByText } = render(<ProfiledCounter />);
const renders: RenderEventInfo[] = [];
const unsubscribe = ProfiledCounter.onRender((info) => {
renders.push(info);
console.log(`Render #${info.count}: ${info.phase}`);
});
fireEvent.click(getByText("Increment"));
fireEvent.click(getByText("Increment"));
expect(renders).toHaveLength(2);
expect(renders[0]!.phase).toBe("update");
expect(renders[1]!.phase).toBe("update");
unsubscribe();
});it('should wait for button click render', async () => {
const Profiled = withProfiler(Counter);
const { getByText } = render(<Profiled />);
// Start waiting BEFORE action
const promise = Profiled.waitForNextRender();
// Trigger action
fireEvent.click(getByText("Increment"));
// Wait for render
const info = await promise;
expect(info.count).toBe(2);
expect(info.phase).toBe("update");
});const Form = () => {
const [value, setValue] = useState("");
const [error, setError] = useState("");
const validate = (v: string) => {
setError(v.length < 3 ? "Too short" : "");
};
return (
<form>
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
validate(e.target.value);
}}
/>
{error && <span>{error}</span>}
</form>
);
};
it('should render efficiently during typing', () => {
const ProfiledForm = withProfiler(Form);
const { getByRole } = render(<ProfiledForm />);
const input = getByRole("textbox");
fireEvent.change(input, { target: { value: "a" } });
fireEvent.change(input, { target: { value: "ab" } });
fireEvent.change(input, { target: { value: "abc" } });
// Should render: mount + 3 updates = 4
expect(ProfiledForm).toHaveRenderedTimes(4);
});const DataFetcher = ({ userId }: { userId: number }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setData)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{data?.name}</div>;
};
it('should handle data fetching renders', async () => {
const Profiled = withProfiler(DataFetcher);
render(<Profiled userId={1} />);
// Wait for: mount + loading + data
await expect(Profiled).toEventuallyRenderTimes(3);
expect(Profiled.getRendersByPhase("update")).toHaveLength(2);
});Check the examples/ directory for comprehensive examples:
-
examples/snapshot/- Snapshot API examples -
examples/memoization/- Memoization testing -
examples/basic/- Basic usage patterns -
examples/performance/- Performance budgets
- API Reference - Full API documentation
- Best Practices - Recommendations
- Hook Profiling - Testing hooks