useCallback
and useMemo
are an attempt to bypass weak spots that come with the functional programming approach chosen with React hooks. In Javascript, each entity, no matter if it is a function, variable, or whatever, is created into the memory when the execution will enter the function's code block. This is a big issue for a React that will try to detect if the component needs to be rendered. The need for rerendering is deducted based on input props and contexts. Let's see a simple example without useCallback
.
const Component = () => {
const [counter, setCounter] = useState(0);
const handleClick = () => {
setCounter(counter + 1);
}
return <div>
Counter:{counter}<br/>
<button onClick={handleClick}>+1</button>
</div>
}
Note that the handleClick -function instance will be created on each function call inside the block, so the event handler's address on each call will be different. The React framework will always see the event handler as changed because of this. In the example above, React will think handleClick as a new value on each call. It simply has no tools to identify it as the same call.
What useCallback
does, it internally stores the first introduced version of the function and returns it to the caller, if the listed variables have not changed.
const Component = () => {
const [counter, setCounter] = useState(0);
const handleClick = useCallback(() => {
setCounter(counter + 1);
}, [])
return <div>
Counter:{counter}<br/>
<button onClick={handleClick}>+1</button>
</div>
}
Now, with the code above, React will identify the handleClick
-event handler as the same, thanks to useCallback
-function call. It will always return the same instance of function and React component rendering mechanism will be happy.
Storing the function internally by the useCallback
will end up with a new problem. The stored instance of the function call will not have direct access to the variables of the current function call. Instead, it will see variables introduced in the initial closure call where the stored function was created. So the call will not work for updated variables. Thats why you need need tell if some used variables have changed. So that the useCallback will store the current function call instance as a new stored instance. The list of variables as the second argument of the useCallback
is listing variables for this functionality. In our example, we need to tell to useCallback
-function that we need to have a fresh version of counter -variable on each call. If we will not do that, the counter value after the call will be always 1, which comes from the original value 0 plus 1.
const Component = () => {
const [counter, setCounter] = useState(0);
const handleClick = useCallback(() => {
setCounter(counter + 1);
}, [counter])
return <div>
Counter:{counter}<br/>
<button onClick={handleClick}>+1</button>
</div>
}
Now we have a working version of the code that will not rerender on every call.
It is good to notice that the useState
-call is here just for the same reason. Function block does not have an internal state, so hooks are using useState
, useCallback
and useMemo
to mimic the basic functionality of classes. In this sense, functional programming is a big step back in history closer to procedural programming.
useMemo
is the same kind of mechanism as useCallback
but for other objects and variables. With it, you can limit the need for component rerender, as the useMemo
-function will return the same values on each function call if the listed fields have not changed.
This part of the new React hooks -approach is definitely the weakest spot of the system. useCallback
is pretty much counterintuitive and really error-prone. With useCallback
-calls and dependencies, it is too easy to end up chasing internal loops. This caveat we did not have with the React Class approach.
The original approach with classes was more efficient after all. The useCallback
will reduce the need to rerender, but it regenerates the function again every time when some of its dependant variables will change, and matching if the variables have changes itself will make overhead. This may cause more rerenders than necessary. This is not the case with React classes.