December 04, 2020
0 strokes bestowed

Helpful Debugging Hooks

edit

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

function useChangeDebugger(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 useChangeDebugger 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) {
  useChangeDebugger(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 useChangeDebugger 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: 62

But what about side effects? Can I also use useChangeDebugger to debug when effects run? Absolutely.

Let's make use of useChangeDebugger inside a useEffectDebugger hook.

function useEffectDebugger(effect, dependencies) {
  useChangeDebugger(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) {
  useChangeDebugger(dependencies)
  return React.useCallback(callback, dependencies)
}

function useMemoDebugger(memoizer, dependencies) {
  useChangeDebugger(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!

Sharing this article on Twitter is a great way to help me out and I really appreciate the support.
+0
Liked the post? Click the beard a few times.
Tags
Newer Post: Use Old School State
Older Post: Headlight Vision

Let's talk some more about JavaScript, 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.

Introduction to State Machines and XState Logo
Introduction to State Machines and XState

Check out my courses!

Liked the post? You might like my video courses, too. Click the button to view this course or go to Courses for more information.
Kyle Shevlin's face, which is mostly a beard with eyes
Kyle Shevlin is a software engineer who specializes in JavaScript, React and front end web development.