Kyle Shevlin

Software Engineer
December 04, 2020
0 strokes bestowed

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: 9

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!


Finished reading?

Liked the post? Give the author a dopamine boost with a few "beard strokes". Click it up to 50 times show your appreciation.

Newer Post: Use Old School State
Older Post: Headlight Vision
Tags
ReactReact HooksJavaScript

Are you, or the company you work for, struggling with something mentioned in this article?
Would you benefit from a live training session?
Let's Talk
Kyle Shevlin's face, which is mostly a beard with eyes
Kyle Shevlin is a software engineer who specializes in JavaScript, TypeScript, React and frontend web development.

Let's talk some more about JavaScript, TypeScript, React, and software engineering.

I write a newsletter to share my thoughts and the projects I'm working on. I would love for you to join the conversation. You can unsubscribe at any time.

Just Enough Functional Programming Logo
Just Enough Functional Programming

Check out my courses!

Liked the post? You might like my courses, too. Click the button to view this course or go to Courses for more information.
I would like give thanks to those who have contributed fixes and updates to this blog. If you see something that needs some love, you can join them. This blog is open sourced at https://github.com/kyleshevlin/blog
alexloudenjacobwsmithbryndymentJacobMGEvanseclectic-codingjhsukgcreativeerikvorhesHaroenvmarktnoonandependabotmarcuslyonsbrentmclarkfederico-fiorinimedayzTowardsDeathFanchGadjonoahmateenbrandonpittmanmattridley
©2023 Kyle Shevlin. All Rights Reserved.