Apr 1st, 2022 - written by Kimserey with .
Callback
hook in React can be used to memoized a callback function to skip unnecessary rendering. In today’s post we will see why we would useCallback
hook.
To understand what useCallback
does, we start by creating a child component:
1
2
3
4
const MyChildComponent: React.FC<{ say: () => string }> = ({ say }) => {
console.log("My child component rendered", new Date().toISOString());
return <p>{say()}</p>;
};
We have a component which takes as input a function say
and would log on each render a message.
We then use this component in a main component:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const App: React.FC = () => {
const [state, setState] = useState(true);
const say = () => {
return state ? "Hello" : "Bye";
};
return (
<>
<p>{state ? "Hello" : "Bye"}</p>
<button onClick={() => setState(!state)}>Toggle</button>
<MyChildComponent say={say}></MyChildComponent>
</>
);
};
As we click on the button, we can see the message printed in the console. On each click, the child component renders.
Now let’s add another state into App
which would just display the current time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const App: React.FC = () => {
const [state, setState] = useState(true);
const [otherState, setOtherState] = useState(new Date().toISOString());
const say = () => {
return state ? "Hello" : "Bye";
};
useEffect(() => {
const interval = setInterval(() => {
setOtherState(new Date().toISOString());
}, 5000);
return () => clearInterval(interval);
}, []);
return (
<>
<p>{state ? "Hello" : "Bye"}</p>
<p>{otherState}</p>
<button onClick={() => setState(!state)}>Toggle</button>
<MyChildComponent say={say}></MyChildComponent>
</>
);
};
We added otherState
with an interval triggering a change of otherState
every five seconds. What we observe is that our child component still render every five seconds even though it does not depend on otherState
.
In fact, it only depends on the function say
but because the parent component is rendered again, React will trigger a render on all child components. This behaviour is the most logical behaviour and will guarantee that all components that need rendering will be up-to-date.
But in our case here, we are defining FunctionComponent
(or FC
, which is a type alias) which depend only on their props. The entire logic inside our components would only need a rendering triggered from the parent if their props change.
This can be solved by using memo
:
1
2
3
4
const MyChildComponent: React.FC<{ say: () => string }> = memo(({ say }) => {
console.log("My child component rendered", new Date().toISOString());
return <p>{say()}</p>;
});
We simply wrap our function component into memo(...)
which would provide a performance boost by memoizing the result of the function component and return it to the parent for renders that don’t include any props change for that component.
And we finally come to the problem:
After wrapping with memo
, we can see that the child component is still rendered on each change of otherState
. The reason for that is that each functions and variables within the parent component App
are recreated at each render, and because functions equality is treated as reference equality, the previous say
will never be the same as the next say
function.
Now that we understand the problem, we can see how useCallback
solves it.
useCallback
HookuseCallback
can be used to create a value holding a reference to same function on each render:
1
2
3
const say = useCallback(() => {
return state ? "Hello" : "Bye";
}, [state]);
We wrap our function into useCallback
, making sure to provide the dependency for when the function would be reevaluated. We end up with the following component:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const App: React.FC = () => {
const [state, setState] = useState(true);
const [otherState, setOtherState] = useState(new Date().toISOString());
const say = useCallback(() => {
return state ? "Hello" : "Bye";
}, [state]);
useEffect(() => {
const interval = setInterval(() => {
setOtherState(new Date().toISOString());
}, 5000);
return () => clearInterval(interval);
}, []);
return (
<>
<p>{state ? "Hello" : "Bye"}</p>
<p>{otherState}</p>
<button onClick={() => setState(!state)}>Toggle</button>
<MyChildComponent say={say}></MyChildComponent>
</>
);
};
With the callback defined, we can then see that the child component is no longer rendered. This is thanks to the fact that the reference to say
remains the same unless state
is being updated.
The second argument of useCallback
is crucial as it defines the dependencies in which the callback gets updated. If we forget to add state
in it, we would have state
always equal to its initial value. It’s recommended to enable exhaustive-deps
when using useCallback
as it prevents this sort of bugs.
And that concludes today’s post!
In today’s post we looked at the problem that useCallback
solves. We started by exposing the problem and then showed how we would solve it. I hope you liked this post and I’ll see you on the next one!