Helpful Debugging Hooks
Update 08/19/2021: I created and published a package of these hooks so you can import them and use them without having to write them yourselves. Install them with:
npm install use-debugger-hooks
You can see the repo here: https://github.com/kyleshevlin/use-debugger-hooks. Hope this helps you with your debugging!
Recently, I needed to do some debugging to improve a few components that were rerendering unexpectedly. In that process of research and trial & error, I came away with a few useful hooks that I want to share with you.
In short, every time props
or state
changes in a React component, it will rerender. This is, unequivocally, a good thing. When a component is rendering more often than expected and it’s an actual performance problem for your app, then you need to determine what props
, state
or other values are changing between renders. In order to do this, you first need a way of retaining the previous value of something.
This can be accomplished with use of useRef
and useEffect
in a custom usePrevious
hook.
function usePrevious(value) {
const ref = React.useRef()
React.useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
This hook isn’t new at all. In fact, you can find it in the official React docs as well as other places. This hook works because useEffect
is always run after render. So during the current render phase we get the previously stored value, then the useEffect
runs to update the value. Pretty cool.
Now that we can store a previous value, we want to determine if the current value and previous value are different, and if they are different, let’s log that information out. To do so, let’s create another custom hook: what I call useLogChanges
:
function useLogChanges(value) {
const previousValue = usePrevious(value)
const changes = getChanges(previousValue, value)
if (changes.length) {
changes.forEach(change => {
console.log(change)
})
}
}
function getChanges(previousValue, currentValue) {
// Handle non-null objects
if (
typeof previousValue === 'object' &&
previousValue !== null &&
typeof currentValue === 'object' &&
currentValue !== null
) {
return Object.entries(currentValue).reduce((acc, cur) => {
const [key, value] = cur
const oldValue = previousValue[key]
if (value !== oldValue) {
acc.push({
name: key,
previousValue: oldValue,
currentValue: value,
})
}
return acc
}, [])
}
// Handle primitive values
if (previousValue !== currentValue) {
return [{ previousValue, currentValue }]
}
return []
}
Great. I pulled out the getChanges
function since we really don’t need the implementation details cluttering the useLogChanges
hook. That reads simply enough now. You can pass this hook any value and log out changes with each render. We could do something like this to debug a component’s props
for example:
function MyComponent(props) {
useLogChanges(props)
return <OtherComponent {...props} />
}
Now, every time MyComponent
rerenders, we will be informed what props changed that caused the rerender.
Here’s a very rudimentary example. This component just generates a random number and passes it to a child component every time you press the button. The child component uses useLogChanges
on its props
. The changes to the props
will be logged out with each render. Open the console, and give it a try.
Be sure to open the console and see the logged out changes!
But what about side effects? Can I also use useLogChanges
to debug when effects run? Absolutely.
Let’s make use of useLogChanges
inside a useEffectDebugger
hook.
function useEffectDebugger(effect, dependencies) {
useLogChanges(dependencies)
React.useEffect(effect, dependencies)
}
Now you have a drop in replacement for useEffect
that will tell you which dependency changed to cause an effect to run. If you really wanted, you could make drop in replacements for any of the standard React library hooks with this pattern.
function useCallbackDebugger(callback, dependencies) {
useLogChanges(dependencies)
return React.useCallback(callback, dependencies)
}
function useMemoDebugger(memoizer, dependencies) {
useLogChanges(dependencies)
return React.useMemo(memoizer, dependencies)
}
// ...etc
That’s all there is to it. Nothing fancy. Just a little hooks composition for you. I hope you find this helpful in your React component debugging!