What I Learned at Work this Week: useCallback

Photo by Jose Antonio Gallego Vázquez from Pexels

About two months ago, I learned that my company’s Frontend Team holds regularly scheduled working group meetings. At these meetings, the group comes together and tries to efficiently knock out a task that has been on the backburner (this week, for example, it was ESLint warnings). The working group is open to any interested party whether or not they’re a member of the Frontend Team and it’s a great way to network, contribute, and learn something new.

And so, for the past two months, I’ve been coming up with excuses not to attend the Frontend Working Group. This week, however, I finally worked up the courage to log in and it was absolutely as advertised. The group was friendly, spend an hour talking about the advantages of different React and Typescript patterns, and was grateful for someone who wanted to help. Unfortunately…I knew far too little about React and about our frontend codebase to actually contribute anything. So, as part of my commitment to network and grow, this week I’ll write about one of the subjects that was discussed: when to use useCallback in React.

React Hooks

import React from ‘react’;function App() {
const [count, setCount] = React.useState(0);
const handleClick = useCallback(() => {
setCount(count + 1)
}, [])
return(
<button onClick={handleClick}>{count}</button>
)
}
export default App;

If you’d like to follow along, check out the React Docs here. Seriously, I’m using a computer where I’ve never written React and it took me about 10 minutes to download VS Code, install npx, start a project, and write this component.

The first declaration we see inside of our functional component is a destructuring of the return value of useState (also known as the State Hook or useState Hook). As I mentioned previously, React uses state to keep track of potentially changing values that we will use to render or provide functionality to our interface. The useState Hook is a function that accepts an argument of some value. It then returns an array of two elements: the value we just provided, and a function that can update that variable. What’s abstracted by this Hook are the implications that come along with React state, most notably that components that reference that state value will be re-rendered when it changes.

Our next function is handleClick, which invokes the setCount function returned by useState to increase our count variable by 1. Finally, we see that our component renders a button which displays the current count. React’s JSX elements can all be assigned an onClick attribute, which uses an event listener to invoke a callback function when the element is clicked. In this case, we’re using handleClick.

It all comes together to render a button that displays a count. The count is increased when the button is clicked and then the button itself is updated to display the new value. These are the basic tenets of React and Hooks: when state changes, our components re-render according to the new state. When our count value goes from 0 to 1, our webpage will reflect the change because it’s actually been re-rendered.

Performance Concerns

Whether it’s useCallback or useEffect, according to my reading, this pattern is overused based on the misconception that it promotes efficiency. Before we get into why that is, let’s look at how these Hooks might be used:

const handleClick = useCallback(() => {
setCount(count + 1)
}, [])

The Hook accepts two arguments: a callback function and an array of dependencies. The function is memoized when it is first defined, meaning it is saved in memory for future reference. The next time our component is rendered, that version of the function will be retrieved unless the values of one of the dependencies has changed. It might be easier to understand when looking at the example from the React Docs:

const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

The main difference here is that our callback function makes use of a set of arguments, which are in turn declared as dependencies. If a or b changes, a new function will be defined. If not, we’ll retrieve the original one from memory.

So what’s the issue? We’re potentially saving ourselves time by pulling an existing function from memory instead of having to redefine it. If that function is the parent of other React components, we’d be saving a lot of unnecessary computations. While this is true, we should also consider:

  1. React is already fast and efficient. It’s built to consistently re-render components and doing so might not put as much stress on our app as we’d expect.
  2. These functions actually have to be defined anyway, even if we don’t end up using them. useCallback, useEffect, and useMemo don’t prevent that from happening, so this will actually be taking up more memory than using the original version of the function.
  3. This is harder to read. A minor optimization might not be worth the additional complexity.

So what’s the point of useCallback?

  1. Hooks like useEffect and useCallback trigger callbacks based on whether dependencies change.
  2. useCallback can reference a previously defined callback rather than a newly created one.

What does this mean for us? Let’s make up an example:

const Button = (var1, var2) => {
useEffect(
() => {
unwrittenCallback(var1, var2)
},
[var1, var2]
)
return <button>Push me</button>
}

In the example, we define a Button component that:

  • accepts two arguments, var1 and var2
  • invokes useEffect
  • renders a button that says “Push me”

In case you’re not familiar, the useEffect Hook runs a callback function after React has updated our page’s DOM. It’s generally used to create “side effects” (or effects, hence the name), meaning something useful to our interface though not necessarily required for the initial render. In any case, we can see that the callback here is a function called unwrittenCallback that uses the two variables passed into the Button component. It doesn’t matter how the callback uses these variables, which is why I didn’t bother to write that out. What’s important is that the variables are listed as dependencies. That means that this code will only be invoked if one of those values change.

And that’s the end of the story if the values we’re passing down are primitive data types, like integers or strings. But what if they’re not? If you’re familiar with JavaScript gotchas, you might have seen something like this before:

const funcA = () => {}const funcB = () => {}funcA === funcB // => false

Though the functions are defined identically, they are not seen as equivalent by JavaScript. They are each a unique object and therefore, if we redefine them each time we invoke Button, the callback inside of useEffect will rerun even though we probably don’t want it to. That is unless…

const Button = (var1, var2) => {
useEffect(
() => {
unwrittenCallback(var1, var2)
},
[var1, var2]
)
return <button>Push me</button>
}
const ButtonParent = () => {
const var1 = useCallback(() => {}, [])
const var2 = useCallback(() => [1, 2, 3], [])

return <Button var1={var1} var2={var2} />
}

Aha! The Hook makes defining our callbacks a bit more complicated, but it saves us from rerunning the useEffect callback in the child component because we are actually retrieving the original functions that were previously memoized. Depending on the complexity or the impact of the useEffect callback, this could make a big difference for us.

Thoughtful Decisions

Sources:

Solutions Engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store