Debugging Why Did You Render Without Third Party Tools
AI generated
{ }
React · Performance · Debugging
Debugging unnecessary re-renders
without installing the why-did-you-render library

Not every extra npm dependency is required to find out why a component re-renders too often. The highlight updates option in React DevTools and a ten line custom hook already cover most cases.

13 min read React DevTools Custom Hooks Re-Render Debugging

1. Why why-did-you-render exists and when you do not need it

The why-did-you-render library patches React at runtime and automatically logs to the console when and why a component re-renders, including a comparison of old and new props or old and new state. This is handy for large codebases where many components need to be monitored at once, but it introduces an extra dependency, a monkeypatch of React itself, and a certain amount of setup effort that is not worth it for every debugging session.

For the more common case where you already suspect one specific component of rendering too often, two much lighter tools are enough: the highlight updates feature built into React DevTools for a quick visual check, and a hand written useWhyDidYouUpdate hook for a precise console log directly inside the affected component, without any extra package in package.json.

2. Enabling highlight updates in React DevTools

The Components tab of React DevTools has a gear icon that opens a settings menu where you can enable the option 'Highlight updates when components render'. Once enabled, DevTools briefly outlines every component in color as soon as it re-renders, while you interact with the application normally, without having to start a formal profiler recording. This immediately reveals which parts of the page light up during a given interaction, even before you know which component to actually suspect.

The border color gives a rough sense of render frequency: blue borders appear for rare renders, green through yellow for more frequent ones, and an area that flashes on every single keystroke in an unrelated search field is a strong sign of missing memoization or an overly broad context dependency. This method provides no numbers and no root cause analysis, but it is the fastest first step to figure out where a more detailed investigation should even start.

3. Where highlight updates reaches its limits

Highlight updates reliably shows the 'that' of a render but not the 'why': you see that a component lit up, but not whether props, state, or context changed and which specific value was responsible. In deeply nested component trees with many areas lighting up simultaneously, it also quickly becomes hard to tell which flash is the actual cause and which is merely a consequence of a render further up the tree.

For those cases you need a method that logs directly to the console which specific values changed between two renders. That exact gap is filled by a small hand written hook that can be dropped into precisely the one component you are currently investigating, like a debugging statement, and removed again afterwards without it ever shipping to production.

4. A custom useWhyDidYouUpdate hook

The core idea is simple: keep the previous render's props in a ref, compare them against the current props on the next render, and log via console.log exactly the properties that differ. A useEffect that runs after every render is sufficient for this, because it is guaranteed to run after rendering and after updating the ref, so the next pass always compares against the actually previous state.

The hook takes the component name for the console output and the full props object as parameters, and compares every single property by reference equality (===). For primitive values like strings or numbers this is sufficient, and for objects and arrays the comparison reliably surfaces exactly the problem that is usually the real cause of unnecessary renders anyway: a reference newly created on every parent render, even though its content stays unchanged.


import { useEffect, useRef } from 'react';

function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();

  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changedProps = {};

      allKeys.forEach((key) => {
        if (previousProps.current[key] !== props[key]) {
          changedProps[key] = {
            from: previousProps.current[key],
            to: props[key],
          };
        }
      });

      if (Object.keys(changedProps).length) {
        console.log('[why-did-you-update]', name, changedProps);
      }
    }

    previousProps.current = props;
  });
}

function ExpensiveList(props) {
  useWhyDidYouUpdate('ExpensiveList', props);
  // ... actual render logic
}

5. Using the hook in practice

To use the hook, a single extra call at the top of the suspicious component is enough, passing the already existing props as the second argument. If the component then re-renders unexpectedly, a line appears in the browser console listing exactly which props changed from their previous value to the new one, including the old and new reference, which immediately shows for functions and objects whether it is the same reference or a new one.

Particularly revealing is the case where the console reports a property as changed even though both logged values look identical in content, for example two objects with the same field values. This is the classic sign of missing memoization in the parent: an object or callback function is re-created on every render of the parent component even though its content never changes, and exactly this pattern should then be fixed with useMemo or useCallback in the parent, not in the child component itself.

6. Extending the hook to cover state and context

The basic version shown so far only compares props, but unnecessary renders can just as easily be caused by internal state or by context updates. The hook can easily be extended by passing it an additional object with the relevant state values alongside the props, handled internally the same way as props, just under its own namespace in the output, so both sources stay clearly distinguishable in the console.

For context updates the extension is slightly more subtle, because a context value does not arrive as a prop but is read directly inside the component via useContext. Here it helps to simply add the read context value as an extra field to the same comparison object passed to the hook, so a context caused render becomes just as visible as one triggered by props.


function useWhyDidYouUpdate(name, values) {
  const previous = useRef();

  useEffect(() => {
    if (previous.current) {
      const changed = {};
      Object.keys(values).forEach((key) => {
        if (previous.current[key] !== values[key]) {
          changed[key] = { from: previous.current[key], to: values[key] };
        }
      });
      if (Object.keys(changed).length) {
        console.log('[why-did-you-update]', name, changed);
      }
    }
    previous.current = values;
  });
}

function CartWidget(props) {
  const [isOpen, setIsOpen] = useState(false);
  const theme = useContext(ThemeContext);

  useWhyDidYouUpdate('CartWidget', { ...props, isOpen, theme });
  // ... actual render logic
}

7. Alternative: React Scan and similar modern tools

Besides why-did-you-render and the built in DevTools features, there are now also modern, very lightweight alternatives like react-scan, which work similarly to highlight updates but require far less setup and do not need React itself to be patched. Such tools are good for a quick overview of an entire page but do not replace the precise analysis that a custom hook delivers for a single, already identified component.

For the use case described in this article, targeted debugging of an already suspected component without an extra dependency, the combination of highlight updates for rough localization and a custom hook for precise root cause analysis remains the most pragmatic path, because both tools are either already available or can be hand written with minimal effort.

8. Cleanly removing the hook again

Since the hook is meant purely for debugging, it should be removed from the component again once the investigation is complete, instead of staying in the code permanently and producing unnecessary console.log output on every render. Anyone who needs the hook more frequently can move it into its own utility file that is only imported in development builds, for example via a condition on process.env.NODE_ENV, so it is guaranteed to have no effect in production.

Alternatively, the hook can be designed to become a no op function in the production build, effectively doing nothing, so it could theoretically remain in the code without causing harm. In practice it is still cleaner to actively remove the debugging statement once the work is done, so the code stays readable long term and nobody has to wonder why a component contains an unusual hook call.

9. Comparing the debugging approaches

All three approaches presented here, highlight updates, the custom useWhyDidYouUpdate hook, and the why-did-you-render library, each have their place depending on how large the codebase is and how precise the root cause analysis needs to be. For a first orientation without any setup, highlight updates is unbeatably fast, for a targeted, repeatable investigation of a single component the custom hook is usually the right choice.

The why-did-you-render library pays off mainly when unnecessary renders need to be watched permanently across many components, for example as part of a larger performance audit, while the other two approaches are meant for punctual, targeted debugging sessions and disappear from the code again afterwards.

Approach Extra dependency Shows the cause Best for
Highlight updates (DevTools) No No, only that it rendered Quick visual first orientation
useWhyDidYouUpdate (custom hook) No Yes, exact prop/state diffs Targeted investigation of one component
react-scan Yes, but lightweight Partially Overview of entire pages
why-did-you-render (library) Yes, patches React Yes, with diff output Ongoing monitoring during development

Mironsoft

React architecture, performance, and Magento frontend integration

React frontends that stay fast instead of slowing down with every feature?

We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.

Performance Audit

Systematically measuring and fixing re-renders, bundle size, and load times.

State Architecture

Cleanly separating context, client state, and server state instead of mixing everything.

Magento Integration

Building robust, type-safe GraphQL or REST integration with Magento.

10. Summary

Re-Render Debugging Without Third Party Tools: The Essentials at a Glance

Highlight updates

DevTools option that colors rendering components during normal use, without a formal recording.

useWhyDidYouUpdate

Small custom hook using useRef and useEffect that logs exactly which props changed between two renders.

Reference equality

The hook compares via ===, which surfaces newly created objects and functions as the cause.

No npm package needed

Both techniques work without an extra dependency or a patch to React.

11. FAQ: Re-Render Debugging Without Third Party Tools: The Essentials at a Glance

1Do I even need why-did-you-render if I already use DevTools?
For most punctual debugging cases, no. The highlight updates option in DevTools quickly shows which components render, and a custom useWhyDidYouUpdate hook provides the exact cause, without patching React itself or adding an extra dependency to the project.
2Where do I enable highlight updates in React DevTools?
The Components tab of DevTools has a gear icon that opens a settings menu. There you can enable Highlight updates when components render, after which rendering components briefly light up in color during normal use of the application.
3Why does the custom hook compare props by reference equality instead of a deep comparison?
Reference equality surfaces exactly the most common problem: objects or functions that get re-created on every render even though their content stays unchanged. A deep comparison would hide this problem, because it would classify content-equal objects as unchanged even though React itself treats them as different and re-renders because of it.
4Can I use the hook for state too, not just props?
Yes, the hook is deliberately generic and accepts any object of values. You can combine props, state, and read context values into a single shared object and pass it to the hook to see all three causes of a render in one place.
5Does the custom hook noticeably slow down my application?
Not noticeably during development, since the comparison only iterates over the properties of a single object and is not an expensive computation. In production the hook should not be active anyway, either because it was removed or because it becomes a no op via a condition on process.env.NODE_ENV.
6What does it mean when the console reports a prop as changed even though the content looks the same?
This is a clear signal of missing memoization in the parent. An object, array, or function is re-created on every render of the parent component and therefore gets a new reference, even though the contained values are identical. The fix is useMemo or useCallback in the parent, not in the affected child component.
7Is react-scan a useful addition to the built in DevTools features?
Yes, for a quick overview of an entire page with many components, react-scan can be more convenient than highlight updates because it sometimes offers additional aggregation. For the targeted investigation of a single, already suspected component, the custom hook usually remains the more precise choice.
8Should the useWhyDidYouUpdate hook stay in the code or be removed again?
Once the debugging session is done, the hook should be removed from the investigated component to avoid unnecessary console output during normal development work. If it is needed permanently, it belongs in its own utility file with an explicit condition on the development environment.
9Does the hook also work with class components?
No, the hook shown here uses useRef and useEffect and therefore only works in function components. For class components the same logic would have to be rebuilt manually in componentDidUpdate, using an instance field instead of the ref to store the previous props.
10Does the hook also detect renders triggered by the parent without any props changing?
No, if neither props nor the state or context values passed in the object change, but the component still renders, the cause usually lies with the parent itself, which re-renders all children on every one of its own renders without memo. In that case the hook shows no changed values, which is itself already a hint that the child component is missing memoization via memo.