Search code examples
javascriptreactjsreact-hooksmemoization

How React.memo works with useCallback


As far as I understand, React.memo is an API that memoize a component: if its props don't change, react use the latest render of that component without comparing it with its previous version. Skipping new render and comparing with old one speed-up the app. Cool.

Now, here's what I don't get: if props don't change, also a not memoized component don't get re-rendered, as I can see from that simple code (use this link to see the demo, the code snippet on this page is a little bit confusing): there's no difference about number of renders between a normal component+usecallback and a memoized one+useCallback. Basically, useCallbacks is all I need, as a normal component doesn't get re-rendered with same props. Then, what I'm I missing? When memo comes in help to optimize?

const { useCallback, useEffect, useState, memo, useRef } = React;

function Child({ fn, txt }) {
  const [state, setState] = useState(0);
  console.log(txt + " rendered!");

  useEffect(() => {
    setState((state) => state + 1);
  }, [fn]);

  return (
    <div style={{ border: "solid" }}>
      I'm a Child
      {!fn && <div>And I got no prop</div>}
      {fn && <div>And I got a fn as a prop</div>}
      <div>
        and I've got rendered <strong>{state}</strong> times
      </div>
    </div>
  );
}

const MemoChild = memo(Child);

function App() {
  const [, setState] = useState(true);

  const handlerOfWhoKnows = () => {};

  return (
    <div className="App">
      I'm the parent
      <br />
      <button onClick={() => setState((state) => !state)}>
        Change parent state
      </button>
      <h3>Ref</h3>
      ref: <Child txt="ref" fn={handlerOfWhoKnows} />
      <h3>test</h3>
      useCB: <Child txt="useCb" fn={useCallback(handlerOfWhoKnows, [])} />
      memo: <MemoChild txt="memo" fn={handlerOfWhoKnows} />
      memo + useCB: <MemoChild txt="memo+useCb" fn={useCallback(handlerOfWhoKnows, [])} />
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>


Solution

  • There are a couple of things going on in that example, but let's start with the headline: Your child labelled "memo+useCb" is exactly right, it's showing the correct way (or at least, a correct way) to avoid having a component re-render unnecessarily. It's doing both necessary things:

    1. Memoizing the component (via memo)

      and

    2. Ensuring that the component's props don't change unnecessarily (via useCallback)

    Your others, the ones labelled "useCb" and "memo", each lack half of the necessary incredients:

    • "useCb" isn't memoized, so it re-renders every time the parent renders

    • "memo" is getting a different fn prop every time, so it re-renders every time the parent renders

    It's the combination of memoizing and ensuring the props don't change (via useCallback, or useMemo, or useRef, etc.) that avoids unnecessary re-renders. Either of them, alone, doesn't, which is what your example is showing.

    One thing about the example that may be misleading you slightly is that you have the components saying "and I've got rendered {state} times" but state isn't counting the number of renders, it's counting the number of times the fn prop changed value, which is not the same thing. A "render" is a call to your function component's function (or the render method of a class component). In your example, the number of renders is shown by the "useCb rendered!" and "memo rendered!" messages, which you see every time the parent renders because we click the button.

    You've said (your emphasis):

    Now, here's what I don't get: if props don't change, also a not memoized component don't get re-rendered...

    Your example is showing you that the un-memoized child does get re-rendered, even when its props don't change: the "useCb" version has stable props, but it still re-renders every time the parent renders. You can see that because, again, it outputs "useCb rendered!" every time you click the button causing the parent to re-render.

    Here's an updated version of your example hopefully showing more clearly when a render happens vs. when a prop change happens, using logging for renders and the component's rendered output for prop changes:

    const { useCallback, useEffect, useState, memo, useRef } = React;
    
    function Child({ fn, txt }) {
        const [fnChanges, setFnChanges] = useState(0);
        const rendersRef = useRef(0);
        ++rendersRef.current;
    
        console.log(`${txt} rendered! (Render #${rendersRef.current})`);
    
        useEffect(() => {
            console.log(`${txt} saw new prop, will render again`);
            setFnChanges((changes) => changes + 1);
        }, [fn]);
    
        return (
            <div style={{ border: "solid" }}>
                {txt}: <code>fn</code> changes: {fnChanges}
            </div>
        );
    }
    
    const MemoChild = memo(Child);
    
    /*export default*/ function App() {
        const [, setState] = useState(true);
    
        const handlerOfWhoKnows = () => {};
        const memoizedHandler = useCallback(handlerOfWhoKnows, []);
    
        return (
            <div className="App">
                <button onClick={() => setState((state) => !state)}>Change parent state</button>
                <div>
                    Not memoizing anything:
                    <Child txt="ref" fn={handlerOfWhoKnows} />
                </div>
                <div>
                    Just memoizing the <code>fn</code> prop, not the component:
                    <Child txt="useCb" fn={memoizedHandler} />
                </div>
                <div>
                    Just memoizing the component, not the <code>fn</code> prop:
                    <MemoChild txt="memo" fn={handlerOfWhoKnows} />
                </div>
                <div>
                    Memoizing <strong>both</strong> the component and the <code>fn</code> prop:
                    <MemoChild txt="memo+useCb" fn={memoizedHandler} />
                </div>
            </div>
        );
    }
    
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(<App />);
    <div id="root"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

    It can be hard to see the logging clearly, though, so here's a second version that includes the number of renders in the component's rendered output (it's very unusual to include non-state in the rendered result, but helpful just for the purposes of illustration in this case, to see what's going on):

    const { useCallback, useEffect, useState, memo, useRef } = React;
    
    function Child({ fn, txt }) {
        const [fnChanges, setFnChanges] = useState(0);
        const rendersRef = useRef(0);
        ++rendersRef.current;
    
        useEffect(() => {
            setFnChanges((changes) => changes + 1);
        }, [fn]);
    
        return (
            <div style={{ border: "solid" }}>
                {txt}: Renders: {rendersRef.current}, <code>fn</code> changes: {fnChanges}
            </div>
        );
    }
    
    const MemoChild = memo(Child);
    
    /*export default*/ function App() {
        const [, setState] = useState(true);
    
        const handlerOfWhoKnows = () => {};
        const memoizedHandler = useCallback(handlerOfWhoKnows, []);
    
        return (
            <div className="App">
                <button onClick={() => setState((state) => !state)}>Change parent state</button>
                <div>
                    Not memoizing anything:
                    <Child txt="ref" fn={handlerOfWhoKnows} />
                </div>
                <div>
                    Just memoizing the <code>fn</code> prop, not the component:
                    <Child txt="useCb" fn={memoizedHandler} />
                </div>
                <div>
                    Just memoizing the component, not the <code>fn</code> prop:
                    <MemoChild txt="memo" fn={handlerOfWhoKnows} />
                </div>
                <div>
                    Memoizing <strong>both</strong> the component and the <code>fn</code> prop:
                    <MemoChild txt="memo+useCb" fn={memoizedHandler} />
                </div>
            </div>
        );
    }
    
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(<App />);
    <div id="root"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

    So again, your example did show the right way to do it, with the "memo+useCb" child.