React Hooks review #5: useCallback

This is chapter two of the seven additional React Hooks series, variants of the first three basic hook, but written to handle edge cases. And still, the goal was simple: summarize all the eleven official React hooks:

useCallback

Returns a memoized version of the callback that only changes if one of the dependencies has changed.

react.js

While rendering a React component, you re-render all the inner values. As React created the useState to prevent re-rendering state variables, they created useCallback for cases you want to memorize a function and prevent re-creation on rendering of dependent sub-components.

Don’t mistake React’s useCallback Hook with React’s memo API. While useCallback is used to memoize functions, React memo is used to wrap React components to prevent re-renderings..

How to useCallback in React

Syntax:

const memoizedCallback = useCallback(callbackFunc,ependencyArr); 
  • callbackFunc: the function we want to memorize.
  • dependencyArr: An array of dependencies.

Before, a short reference equality reminder

Before starting, a short reminder that a function variable is a reference to the location of the function. Passing a function will not duplicate it, just duplication it’s reference.

const writeLog = () => console.log;
const log1 = writeLog();
const log2 = writeLog();

log1 == log2;

While re-creating a function creates a new object: a new reference to a new copy of the code in the memory.

const writeLog = () => v => console.log(v);
const log1 = writeLog();
const log2 = writeLog();

log1 != log2;

This is why we can use the JS set, which keeps only unique values, to have a visual sense of the useCallback.

Prevent re-creating a function

React component re-renders each time something changes. So all the local variables and methods are recreated. In this example, there is a re-render button, and each time it happens, a new function is added to the set.

const functionsCounter = new Set()
const UseCallback = ()=>{
    const [counter,setCounter] = React.useState(1);

    const someFunction = () => {};
    functionsCounter.add(someFunction);

    return <div>
        <div>Number of function creations: {functionsCounter.size}</div>
        <div>Number of component renders: {counter}</div>
        <button onClick={()=>{setCounter(counter + 1)}}>Re-render</button>
    </div>
}

To prevent the re-creation of the same function we can wrap it with the useEffect hook. This time, we add the same reference to the set: reference to the memoized function. So the size of the set will not increase.

const functionsCounter = new Set()
const UseCallback = ()=>{
    const [counter,setCounter] = React.useState(1);

    const someFunction = React.useCallback(() => {},[]);
    functionsCounter.add(someFunction);

    return <div>
        <div>Number of function creations: {functionsCounter.size}</div>
        <div>Number of component renders: {counter}</div>
        <button onClick={()=>{setCounter(counter + 1)}}>Re-render</button>
    </div>
}

If we want to re-create the function according to some dependency, we can use the second argument that triggers the useEffect to re-execute.

const functionsCounter = new Set()
const UseCallback = ()=>{
    const [renderer,setRenderer] = React.useState(1);
    const [refresher,setRefresher] = React.useState(1);

    const someFunction = React.useCallback(() => {

    },[refresher]);
    functionsCounter.add(someFunction);

    return <div>
        <div>Number of function creations: {functionsCounter.size}</div>
        <button onClick={()=>{setRenderer(renderer + 1)}}>Re-render</button>
        <button onClick={()=>{setRefresher(refresher + 1)}}>Re-render and re-create</button>
    </div>
}

Is this useful: until now, in most cases – not. And usually, it even will cost more than it will save. Since the function rendering is very fast and optimized. And using useCallback requires to keep the function instead of sending it to the garbage collector. And will force React to case a copy of the dependency list in the memory.

Prevent re-rendering sub components

Without useCallback, sending a function as a prop will rerender the sub-component each time the function had changed / each time the parent had been re-rendered. With useCallback the function gets defined only if any of its dependencies change, so the sub component will not need to re-render. To demonstrate this we will use Reacts momo that memorize a component.

const StaticComp = React.memo(({callbackFunc})=>{
    console.log('Rendering a static CompWith Memo');
    return <p onClick={callbackFunc}>Click to exe the callback</p>
})

const UseCallback = ()=>{
    const [counter,setCounter] = React.useState(0);

    const callbackFunc = () => {
        console.log('callback');
    };

    return <div>
        <button onClick={()=>{setCounter(counter + 1)}}>Re-render</button>
        <StaticComp callback={callbackFunc} />
    </div>
}

Adding the useCallback will prevent the re-rendering of the sub component:

const StaticComp = React.memo(({callbackFunc})=>{
    console.log('Rendering a static CompWith Memo');
    return <p onClick={callbackFunc}>Click to exe the callback</p>
})

const UseCallback = ()=>{
    const [counter,setCounter] = React.useState(0);

    const callbackFunc = React.useCallback(() => {
        console.log('callback');
    },[]);

    return <div>
        <button onClick={()=>{setCounter(counter + 1)}}>Re-render</button>
        <StaticComp callback={callbackFunc} />
    </div>
}

But before you memorise everything…

As mentioned, this hook is used for optimizations, and in most cases, there is no need to use it. and sometimes the cost is bigger than the benefit. So as always, it’s better to know what you are doing.

Performance optimizations are not free. They ALWAYS come with a cost but do NOT always come with a benefit to offset that cost.

When to useMemo and useCallback

Somehow I have the feeling that in this post I broke the record of “re” using the “re”.