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.
Table of Contents
- 1. Why why-did-you-render exists and when you do not need it
- 2. Enabling highlight updates in React DevTools
- 3. Where highlight updates reaches its limits
- 4. A custom useWhyDidYouUpdate hook
- 5. Using the hook in practice
- 6. Extending the hook to cover state and context
- 7. Alternative: React Scan and similar modern tools
- 8. Cleanly removing the hook again
- 9. Comparing the debugging approaches
- 10. Summary
- 11. FAQ
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.