简体   繁体   中英

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>

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.

Additional to the @TJ Crowder's answer,

Your sample project runs in <StrictMode> so React will run a component twice in dev mode to validate things. To understand the issue, first lets remove <ScrictMode temporally from the index.js

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?

By using memo you are telling the reconciliation algorithm to NOT to go down and render the Child component again if the props are same. But in a normal component without memo , the reconciliation algorithm will traverse through the tree and call your child component ( because that's what algorithm supposed to do ). Once the traversing(rendering) is done, react will flush the changes to the DOM. As there's no changes will be committed to the DOM. but using memo you are speed up the traversing process.


NOTE:

In React, 'render' doesn't mean updating the DOM elements. rendering mean calling your function component or Component.render() method. This is not an expensive process so React will do it but React won't blindly flush the changes to the DOM. You can validate this by open the inspect element. When you expand the inspect element, click the button, you won't see any changes(create/delete animation with purple color highlight on dom elements) happens. By using memo you manually stop the render() and move to the sibling element ( if available ) to continue re render instead of going into the child element. This is also why you cannot use console.log to actually test the performance of the react app. console.log is a side effect. React strictly telling you to not add side effect to the render phase ( top level of your function body ). To handle side effects you need to use useEffect .


Now lets see the issues in your code, Based on your sandbox code, after removing <StrictMode> and reload the preview, we will get the following logs on the console,

For this component,

    <div className="App">
      ref: <Child txt="ref" fn={handlerOfWhoKnows} />
      useCB: <Child txt="useCb" fn={useCallback(handlerOfWhoKnows, [])} />
      memo: <MemoChild txt="memo" fn={handlerOfWhoKnows} />
      memo + useCB: <MemoChild txt="memo+useCb" fn={useCallback(handlerOfWhoKnows, [])} />
    </div>

It will log following on mount,

ref rendered!
useCb rendered! 
memo rendered! 
memo+useCb rendered!

ref rendered! 
useCb rendered! 
memo rendered! 
memo+useCb rendered! 

1. log print twice on mount

The reason why you see each logs print twice is, you use a useEffect inside your <Child/> component.

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

Since the state change triggered, React will re run the <Child /> for the 2nd time as well. It has nothing to do with the props. If we remove this useEffect , now you can see the mount log as,

ref rendered! 
useCb rendered! 
memo rendered! 
memo+useCb rendered! 

2. Clicking the button

After remove the StrictMode and useEffect in the child, and click the button. This time the log will print

ref rendered! // rendered due to the prop changed
useCb rendered! // rendered due to the prop changed
memo rendered! // rendered due to the prop changed

This time you can see, memo+useCb is not printed. The next issue is why,

3. Why memo rendered! in 2nd step.

This is because you didn't memoized the fn so it will recreate everytime when rendering. So props change -> component re render

So code should be,

const handlerOfWhoKnows = useCallback(() => {}, []);

<MemoChild txt="memo" fn={handlerOfWhoKnows} />

Now with the changes, the component will looks like,

  const handlerOfWhoKnows = useCallback(() => {}, []);

  return (
    <div className="App">
      ref: <Child txt="ref" fn={handlerOfWhoKnows} />
      useCB: <Child txt="useCb" fn={handlerOfWhoKnows} />
      memo: <MemoChild txt="memo" fn={handlerOfWhoKnows} />
      memo + useCB: <MemoChild txt="memo+useCb" fn={handlerOfWhoKnows} />
    </div>
  );

Now when we click the button, we will only see,

ref rendered! 
useCb rendered! 

So the answer for your question,

When memo comes in help to optimize?

memo helps to manually stop the further rendering if the props are same. Reconciliation algorithm will stop traversing once it see a memo and props are same. It will complete the rendering the tree or it will continue to render the sibling element.

Also using the memo too much can lead to performance issues as well because memo it self does some process to figure out whether to render the child or not. Use useCallback and useMemo carefully.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM