React Callback Hook React Typescript

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.

The Problem

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 Hook

useCallback 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!

Conclusion

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!

External Sources

Designed, built and maintained by Kimserey Lam.