DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

I Built a Tool That Shows You Exactly Why a React Component Re-renders

"Why did this component re-render?" is the React question everyone asks and almost no one can answer at a glance — because the answer is invisible. So I built a tool that makes it visible.

▶ Live demo: https://react-render-visualizer.vercel.app/
Source (React 19 + TS + Vite): https://github.com/dev48v/react-render-visualizer

The component tree on screen is a real React tree. Every node flashes when it actually renders, counts its renders, and labels why it rendered. A side panel keeps a running wasted renders counter.

The three rules, made visible

There are really only three reasons a component re-renders, and the tool shows each one:

  1. A parent re-render re-renders all of its children — by default. With React.memo off, click the tick++ button and the entire tree flashes, even components whose props never changed. The wasted counter climbs.
  2. React.memo skips the re-render when props are unchanged. Turn memo on, click tick++ → only Content → Counter re-render (they're the ones that actually use tick); Header and Sidebar skip. Wasted renders: 0.
  3. A fresh object/array/function prop defeats memo. Turn "stable props" off so the parent hands down a brand-new style object every render, and click again → Sidebar and Content re-render despite being memoized, because memo's shallow compare sees a changed reference. That's the useMemo/useCallback lesson, concrete.
memo stable props tick++ re-renders
off everything
on on only Content → Counter
on off Sidebar + Content too (new style ref)

How it's built

The whole thing hinges on a small hook:

function useTracker(name: string, watch: Record<string, unknown>) {
  const n = useRef(0);
  const prev = useRef(watch);
  n.current++;

  let reason: string;
  if (n.current === 1) reason = "mount";
  else {
    const changed = Object.keys(watch).filter(k => watch[k] !== prev.current[k]);
    reason = changed.length
      ? "props changed: " + changed.join(", ")
      : "parent re-rendered (no prop change)";
  }
  prev.current = watch;

  useEffect(() => { record(name, reason); /* + flash the node */ });
  return reason;
}
Enter fullscreen mode Exit fullscreen mode

It diffs this render's watched props against the previous render to classify the cause. One subtlety worth stealing: the render events go into a tiny external store read with useSyncExternalStore, not React state. If the panels used useState, recording a render would itself trigger renders and pollute the very counts you're measuring. An external store updates the panels without re-rendering the tree under test.

No StrictMode, on purpose — StrictMode intentionally double-invokes render in dev, which would double every count.

Why a playground beats an article (even this one)

You can read "memo does a shallow prop comparison" ten times and still ship a style={{...}} that silently breaks it. Clicking a button and watching the wasted counter jump from 0 to 12 the instant you pass an inline object teaches it in one go.

One npm install, zero UI dependencies. If it made re-renders click for you, a star helps others find it: https://github.com/dev48v/react-render-visualizer

Top comments (0)