Avoiding Accidental Re-Renders with the useCallback Hook

Jamund Ferguson
InstructorJamund Ferguson

Share this video with your friends

Send Tweet

With React hooks it's common to write callback functions in our component body. Event handlers are a common example of this. Most of the time they work great, however, when you're passing those event handlers down to a child component or using them as dependencies in another hook such as useEffect they can be a little bit tricky. Because the functions are re-created on every single render, the reference to that function changes. And each time it changes when it's used as a prop or a dependency, it will force an update. The useCallback hook was created to provide stable references to callback functions to avoid this problem. It's not always needed, but this lesson will show you how to use it when it's needed.

In this lesson we start off by completing the conversion of our <AmountField> to using the Redux Hooks API, but in doing so introduce a new problem: our debouncing stops working. It turns out that by defining our dispatched action inline our useMemo hook was relying on an unstable function reference. Each time we changed the AmountField, we ended up creating a new debounced function and immediately firing off the call to update our rate table. To solve this problem we wrap our dispatched action function in useCallback.

It's important to note that useCallback is not needed for every callback function you create and should only be used when there are real performance issues at hand.

Jamund Ferguson: [0:05] Open up AmountField.js, and import { useSelector, useDispatch } from "react-redux". At the top of the component type const dispatch = useDispatch.

[0:17] On the next line, type const amount = useSelector(getAmount), then scroll down to mapDispatchToProps and grab the changeAmount() method. We'll just make that a variable in our component, const changeAmount = (newAmount) => dispatch(amountChanged(newAmount)). We can then remove our props and our Amount Field works as it did before.

[0:44] If we scroll down here, we can get rid of mapDispatchToProps, mapStateToProps and our propTypes. For both of those arguments, in our connect function, we'll simply remove them, and our component looks better than ever, except we've introduced one problem. Our debouncing no longer works.

[1:01] As I type into the Amount Field, you can see that the Rates Table is instantly updated. It's supposed to be waiting until I'm done typing. That's because we brought the changeAmount function directly into our component body.

[1:13] There's nothing wrong with that, but because the useMemo hook has a dependency on changeAmount, every single time AmountField gets rendered, changeAmount is recreated and this reference is broken, causing a brand-new debounce function to be created, and causing the entire component to re-render, which ultimately is ruining the debounce effect.

[1:31] In order to work around this problem, we have two options. One, we can simply remove changeAmount from the dependency array, which is actually fine, because we only need this to be run the first time the component's rendered.

[1:43] If we want to keep it, however, pull in another hook, called useCallback from "react," and wrap our callback in that, const changeAmount = useCallback, pass the callback exactly as it's written, and then at the end we also have a dependency array. This one is going to have no dependencies.

[2:01] With that in place, the changeAmount function now has what's considered a stable reference, meaning each time that this component is rendered, it's no longer going to change this variable. Therefore, it won't trigger an updated version of onAmountChanged, and then therefore it won't trigger an additional re-render of our amount field. As I quickly type in here, it waits to update our Rates Table until I'm done typing.

[2:25] UseCallback is useful in a couple situations. When you have a function that's being passed in as a dependency to useMemo or useEffect, it can be useful. It can also be useful if you're passing a callback down to a child component.

[2:37] If it's another React component that we created and we're passing down a callback, we want to make sure to wrap it in useCallback to prevent that child component from re-rendering unnecessarily.