Skip to content

Examples

Oleg edited this page Dec 18, 2025 · 4 revisions

Real-world usage patterns and scenarios.

Table of Contents


Basic Examples

Component Render Counting

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();
});

Phase Detection

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");
});

Snapshot Testing

Since: v1.10.0

The Snapshot API helps test optimization patterns by creating render baselines.

Single Render Per Action

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();
});

Testing Optimization with Snapshots

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();
});

Iterative Testing Workflow

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
});

Performance Budget with Snapshots

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);
});

Extended Matchers (v1.11.0)

Since: v1.11.0

Extended matchers provide more flexible assertions for snapshot-based testing. See Snapshot API for full documentation.

Sync Matchers

toHaveRerendered() - At Least One

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();
});

toHaveRerendered(n) - Exact Count

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);
});

Async Matchers

toEventuallyRerender()

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();
});

toEventuallyRerenderTimes(n)

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,
  });
});

Example Test File

For comprehensive examples, see:


Testing Memoization

React.memo

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.


useMemo

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);
});

Async Testing

Waiting for Async Updates

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();
});

Using waitForRenders

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);
});

Real-Time Tracking

Subscribe to Renders

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();
});

Wait for Next Render

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");
});

Complex Scenarios

Form Validation

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);
});

Data Fetching

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);
});

More Examples

Check the examples/ directory for comprehensive examples:


See Also

Clone this wiki locally