December 04, 2020

Helpful Debugging Hooks

edit

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!

Current Value: 46

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!


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Want to read more?
Older Post: Headlight Vision
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Data Structures and Algorithms
Data Structures and Algorithms
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.