Welcome to A Look at React Hooks, a beginner-friendly series on React Hooks. In this article, let's look at the useCallback
Hook.
What is useCallback?
At a glance, it seems that this Hook is pretty straightforward. It accepts 2 arguments: a callback function and an array of dependencies. It returns a memoized callback function when any value in the dependency array has changed.
In code, it looks like (from React Hooks docs):
const memoizedCallback = useCallback(
() => {doSomething(a, b);}, //callback function
[a, b], //dependency array
);
Memoization
The word 'memoized' or 'memoization' is an optimization technique to speed up expensive function calls by returning cached results instead of re-computing if the inputs of the function are the same.
For this Hook, understanding why we need to memoize a callback function is essential, so we know when to use it. So let illustrate that with a simple example.
An Example
Take a look at this simple app. It consists of 3 components, Todo
, Number
and Counter
.
The Todo
component displays items that were passed from props. The Number
component displays the current value of number
while the Counter
component is responsible for the 2 buttons that can change the value of number
. These 3 components are wrapped inside App.js
as shown below.
function App() {
console.log("Render App");
//init items for Todo
const [items, setItems] = useState([
"1. Some todo",
"2. Some todo",
"3. Some todo"
]);
//init number for Number and Counter
const [number, setNumber] = useState(0);
//add items for Todo
const add = () => {
setItems(() => [...items, "New todo"]);
};
//handler function for Counter
const increase = () => {
setNumber(number + 1);
};
//handler function for Counter
const decrease = () => {
setNumber(number - 1);
};
return (
<div>
<Todo items={items} add={add} />
<Number number={number} />
<Counter incr={increase} decr={decrease} />
</div>
);
}
As seen in the code, all the states and functions are within App.js
while the 3 child components are merely acting as separate containers for this example.
In each component, we have a console.log
statement to track when they are rendered.
In todo.js
function Todo(props) {
console.log("Render Todo");
return (
<div>
<h2>My Todo</h2>
{props.items.map((item) => {
return <p>{item}</p>;
})}
<button onClick={props.add}>Add Todo</button>
</div>
);
}
In number.js
function Number(props) {
console.log("Render Number");
return (
<div>
<h2>Counter</h2>
<p>The number is: {props.number}</p>
</div>
);
}
In counter.js
function Counter(props) {
console.log("Render Counter");
return (
<div>
<button onClick={props.incr}>Increase</button>
<button onClick={props.decr}>Decrease</button>
</div>
);
}
If we run our app, notice in the clip below that the console logs when a component is re-rendered. If we only click the Counter
buttons and update the number
value, the Todo
component also re-renders along with Number
and Counter
. It is unnecessary for Todo
to re-render if its values stay the same.
It goes for the same when an item is added to the Todo component and the items
array gets updated. Both Counter and Number gets unnecessarily re-rendered even though they have nothing to do with items
.
This can be an issue if a component is very huge and needs to load over a hundred items on its list. If it keeps being re-rendered even when its items do not change, it can cause performance issues in the app. There is no need to re-render the components that unrelated to a particular state update.
Solution
React.memo()
is a built-in React feature that renders a memoized component and skip unnecessary re-rendering. So each component will only re-render if it detects a change in their props.
So, we can wrap the component with React.memo()
in its export line:
export default React.memo(Component);
Wrap React.memo around the Todo, Number and Counter components. At this point, you might think: Great! That should be all. But let's see the app in action:
Uh-oh! Why is it still rendering unnecessary components? When the 'Add Todo' button is clicked, we expect only App
and Todo
to re-render. Instead, we get App
, Todo
and Counter
, but Number
is behaving correctly.
Why is this so?
Let's go back and see how App
returns the 3 components.
return (
<div>
<Todo items={items} add={add} />
<Number number={number} />
<Counter incr={increase} decr={decrease} />
</div>
);
As you can see, the 3 functions written in App
: add
, increase
and decrease
are passed as props in Todo
and Counter
. Notice that only the value number
is passed into Number
as props.
In React, whenever a component re-renders, a new instance of the function in it gets generated. Therefore, every time App
renders, add
, increase
and decrease
are re-created. So their references now points to different functions in memory. Hence, in terms of referential equality, the functions before re-render are not the same as the functions after the re-render. See diagram below to visualize.
As a result, when 'Add Todo' button is clicked:
App
gets re-rendered.- The
items
array and the references foradd
,increase
anddecrease
gets updated. - React.memo accounts for these changes and re-renders the components with
items
,add
,increase
anddecrease
as their props.
On the other hand, Number
does not get re-rendered when 'Add Todo' button is clicked because there is no change to the number
prop.
So how do we prevent the reference of the functions the same?
useCallback to the rescue
As previously mentioned, the Hook takes a callback function as its argument and a dependency array as its second. To solve the issue in our example, we simply need to wrap our handler functions in App.js
: add
, increase
and decrease
inside the Hook. This prevents the unnecessary re-rendering behaviour because it ensures the same callback function reference is returned when there is no change in their dependency.
For example, let's edit the add
function first.
//add items for Todo - before
const add = () => {
setItems(() => [...items, "New todo"]);
};
//add items for Todo - after
const add = useCallback(() => {
setItems(() => [...items, "New todo"]);
}, [items])
Now, the function add
will only be updated if items
change. Do the same for increase
and decrease
functions. These functions should only be updated when number
changes. They should look like:
const increase = useCallback(() => {
setNumber(number + 1);
}, [number]);
const decrease = useCallback(() => {
setNumber(number - 1);
}, [number]);
Result
Yay! Now only the relevant components will re-render. add
only result in Todo
updating and re-rendering. And increase
or decrease
will re-render Number
and Counter
components.
Conclusion
And that's the gist of this Hook! Thanks for reading this article. I hope it was helpful for React beginners. Please feel free to ask questions in the comments below. Ultimately, practising and building projects with this Hook will help anyone to pick it up faster.
The next Hook in this series will be: useMemo
. It will be the last basic Hook in this series. After that, this series will continue for advanced/custom Hooks. Stay tuned and cheers!