14

I'm trying to make a simple example that follows the React Hooks example in the doc at page https://reactjs.org/docs/hooks-reference.html#usecallback

Without useCallback the code works find as is in this example:

import React, { useCallback } from "react";

function Test(props) {
  function doSomething(a, b) {
    console.log("doSomething called");
    return a + b;
  }

  return (
    <div>
      {Array.from({ length: 3 }).map(() => (
        <div>{doSomething('aaa','bbb')}</div>
      ))}
    </div>
  );
}

export default Test;

However, when I add what I think is the right code for useCallback as follows, I get an error (a is not defined)

import React, { useCallback } from "react";

function Test(props) {
  function doSomething(a, b) {
    console.log("doSomething called");
    return a + b;
  }

  const memoizedCallback = useCallback(
    () => {
      doSomething(a, b);
    },
    [a, b]
  );

  return (
    <div>
      {Array.from({ length: 3 }).map(() => (
        <div>{memoizedCallback("aaa", "bbb")}</div>
      ))}
    </div>
  );
}

export default Test;

The problem code is here:

https://stackblitz.com/edit/react-usememo2?file=Hello.js

Peter Kellner
  • 14,748
  • 25
  • 102
  • 188

1 Answers1

20

The intent of useCallback is to be able to leverage props or state that are in the current scope and that may change on re-render. The dependency-array then tells React when you need a new version of the callback. If you are trying to memoize an expensive computation, you need to use useMemo instead.

The example below demonstrates the differences between useCallback and useMemo and the consequences of not using them. In this example I am using React.memo to prevent Child from re-rendering unless its props or state change. This allows seeing the benefits of useCallback. Now if Child receives a new onClick prop it will cause a re-render.

Child 1 is receiving a non-memoized onClick callback, so whenever the parent component re-renders, Child 1 always receives a new onClick function so it is forced to re-render.

Child 2 is using a memoized onClick callback returned from useCallback and Child 3 is using the equivalent via useMemo to demonstrate the meaning of

useCallback(fn, inputs) is equivalent to useMemo(() => fn, inputs)

For Child 2 and 3 the callback still gets executed every time you click on Child 2 or 3, useCallback just ensures that the same version of the onClick function is passed when the dependencies haven't changed.

The following part of the display helps point out what is happening:

nonMemoizedCallback === memoizedCallback: false|true

Separately, I'm displaying somethingExpensiveBasedOnA and a memoized version using useMemo. For demonstration purposes I'm using an incorrect dependency array (I intentionally left out b) so that you can see that the memoized version doesn't change when b changes, but it does change when a changes. The non-memoized version changes whenever a or b change.

import ReactDOM from "react-dom";

import React, {
  useRef,
  useMemo,
  useEffect,
  useState,
  useCallback
} from "react";

const Child = React.memo(({ onClick, suffix }) => {
  const numRendersRef = useRef(1);
  useEffect(() => {
    numRendersRef.current++;
  });

  return (
    <div onClick={() => onClick(suffix)}>
      Click Me to log a and {suffix} and change b. Number of Renders:{" "}
      {numRendersRef.current}
    </div>
  );
});
function App(props) {
  const [a, setA] = useState("aaa");
  const [b, setB] = useState("bbb");

  const computeSomethingExpensiveBasedOnA = () => {
    console.log("recomputing expensive thing", a);
    return a + b;
  };
  const somethingExpensiveBasedOnA = computeSomethingExpensiveBasedOnA();
  const memoizedSomethingExpensiveBasedOnA = useMemo(
    () => computeSomethingExpensiveBasedOnA(),
    [a]
  );
  const nonMemoizedCallback = suffix => {
    console.log(a + suffix);
    setB(prev => prev + "b");
  };
  const memoizedCallback = useCallback(nonMemoizedCallback, [a]);
  const memoizedCallbackUsingMemo = useMemo(() => nonMemoizedCallback, [a]);
  return (
    <div>
      A: {a}
      <br />
      B: {b}
      <br />
      nonMemoizedCallback === memoizedCallback:{" "}
      {String(nonMemoizedCallback === memoizedCallback)}
      <br />
      somethingExpensiveBasedOnA: {somethingExpensiveBasedOnA}
      <br />
      memoizedSomethingExpensiveBasedOnA: {memoizedSomethingExpensiveBasedOnA}
      <br />
      <br />
      <div onClick={() => setA(a + "a")}>Click Me to change a</div>
      <br />
      <Child onClick={nonMemoizedCallback} suffix="1" />
      <Child onClick={memoizedCallback} suffix="2" />
      <Child onClick={memoizedCallbackUsingMemo} suffix="3" />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Edit useCallback and useMemo

Here's a related answer: React Hooks useCallback causes child to re-render

Ryan Cogswell
  • 75,046
  • 9
  • 218
  • 198
  • On the first example, it still outputs the console.log 3 times. What am I not getting? (hard question I guess) – Peter Kellner Feb 28 '19 at 21:26
  • 1
    You're just misunderstanding the purpose of `useCallback`. `useCallback` does not memoize the results of the function -- it memoizes the function itself. In the first example there is no good reason to use it, but in the second example it would prevent the `onClick` property of `Child` from changing on re-renders. If you want to memoize the results (to prevent redundant executions of an expensive function), you should be be using [`useMemo`](https://reactjs.org/docs/hooks-reference.html#usememo). – Ryan Cogswell Feb 28 '19 at 21:58
  • you are so right I don't get it. I'm stymied by this comment in the docs: `useCallback(fn, inputs) is equivalent to useMemo(() => fn, inputs).` I'm putting courseware together and am a little lost, looking for good examples. – Peter Kellner Feb 28 '19 at 22:08
  • 1
    I've replaced my example with one that is no longer based on your code, but that should help clear things up if you spend enough time studying it, interacting with it, and viewing the results. – Ryan Cogswell Feb 28 '19 at 23:03