how to use React.memo with a Component contains children
Asked Answered
S

4

25

I have two Components, and I wrapped Parent with React.memo:

Child

const Child = ()=> <div>I'm Child</div>

export default Child

Parent

const Parent = (props)=> <div>{props.children}</div>

export default React.memo(Parent)

Use in App:

const App = () => {
  const [count, setCount] = useState(0)

  return(
    <div>
      <button onClick={()=>setCount(count+1)}></button>

      <Parent>
        <Child></Child>
      </Parent>
    </div>
  )
}

Click the button, result:

The Parent Component will rerender, so the memo not working because it's children is a function component

So, how can I prevent rerender?

I know a way to solve by useMemo, but it's ugly and not friendly, do you have better ideas?

const App = () => {
  const [count, setCount] = useState(0)

  const children = useMemo(()=><Child></Child>,[])

  return(
    <div>
      <button onClick={()=>setCount(count+1)}></button>

      <Parent>
        {children}
      </Parent>
    </div>
  )
}
Scrofulous answered 13/3, 2020 at 11:19 Comment(5)
The Child component is rendered by the App component. So either you move the rendering of Child into the Parent component or you also wrap Child in React.memo().Chloe
no, it wont work, I tried...Scrofulous
Okay so your Parent will still re-render because its children prop technically changes when App re-renders, which is why even React.memo() doesn't help there. Basically the best way around that seems to be what you did using useMemo. You have to memorize what you pass to Parent as children or it will re-render.Chloe
Thank you, there is another way React.memo(Parent, ()=>true). But it's unsafe...Scrofulous
What if you have a family tree? I mean, <Parent><Child1/><Child2><Granchild1></Grandchild1></Child2></Parent> and so on, it goes deep many layers (my app is a bunch of mini components). How do I deal with this complexity?Emilyemina
G
7

Wrap your <Child /> with React.memo:

const Child = ()=> {
  console.log('render') // fires only once - on initial render
  return <div>I'm Child</div>
}

const MChild = React.memo(Child);
const Parent = (props)=> <div>{props.children}</div>
const MParent = React.memo(Parent)

const App = () => {
  const [count, setCount] = useState(0);

  return(
    <div>
      <button onClick={()=>setCount(count+1)}>increment {count}</button>
      <MParent>
        <MChild></MChild>
      </MParent>
    </div>
  )
}

render(<App />, document.getElementById('root'));
Goatfish answered 29/6, 2020 at 11:15 Comment(5)
But what happens if you log inside Parent?Implicit
For some reason, i don't think this would work. The children of MParent still changes. I would think a useMemo to cache the entire <Child /> can work, like in Maxime's answer.Devonne
The parent will always re-render when has children as prop, so if you have any functions or objects in parent, just use useMemo and useCallbackNoma
You may have misread the question. Yes MChild does not re-render because it has no children. But MParent re-renders every time despite being memoized, because its children prop breaks memoization. See codepen.io/tamlyn/pen/ExEBPKdSymbolism
I don't know why this was chosen as a selected answer. It obviously doesn't work because MParent still rerenders every time.Idiot
C
15
const children = useMemo(()=><Child></Child>,[])

Is the easiest way to go. Using memo(Child) wont work since jsx in fact returns a new object whenever you call <Child />. React.memo by default just use simple shallow comparison so there really is no other direct way to solve it. You can create your own function that would eventually support children and pass it to React.memo(MyComp, myChildrenAwareEqual).

Cohberg answered 30/7, 2020 at 6:14 Comment(0)
G
7

Wrap your <Child /> with React.memo:

const Child = ()=> {
  console.log('render') // fires only once - on initial render
  return <div>I'm Child</div>
}

const MChild = React.memo(Child);
const Parent = (props)=> <div>{props.children}</div>
const MParent = React.memo(Parent)

const App = () => {
  const [count, setCount] = useState(0);

  return(
    <div>
      <button onClick={()=>setCount(count+1)}>increment {count}</button>
      <MParent>
        <MChild></MChild>
      </MParent>
    </div>
  )
}

render(<App />, document.getElementById('root'));
Goatfish answered 29/6, 2020 at 11:15 Comment(5)
But what happens if you log inside Parent?Implicit
For some reason, i don't think this would work. The children of MParent still changes. I would think a useMemo to cache the entire <Child /> can work, like in Maxime's answer.Devonne
The parent will always re-render when has children as prop, so if you have any functions or objects in parent, just use useMemo and useCallbackNoma
You may have misread the question. Yes MChild does not re-render because it has no children. But MParent re-renders every time despite being memoized, because its children prop breaks memoization. See codepen.io/tamlyn/pen/ExEBPKdSymbolism
I don't know why this was chosen as a selected answer. It obviously doesn't work because MParent still rerenders every time.Idiot
D
6

Move <Parent><Child/></Parent> into a separate component, and memoize that component instead:

const Family = memo(() => <Parent><Child/></Parent>);

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count => count + 1)}>{count}</button>
      <Family />
    </>
  )
}

demo

Dickdicken answered 4/6, 2021 at 12:16 Comment(1)
I think this should be the correct answer. This helped me fix a common list case where const MyComp = memo((item) => <Parent item={item}><Child/></Parent>); and const renderItem = useCallback((item) => <MyMemoizedComp ... />) and <List renderItem={renderItem} />Concertmaster
D
2
const Memo = React.memo(({ children, value }) => {
  return children
}, (prev, next) => prev.value === next.value)

And then to use it

function Title() {
  const [count, setCount] = useState(0)
  const onClick = () => {
    setCount(c => c + 1)
  }

  const a = ''

  return (
    <>
      <div onClick={onClick}>{count}</div>
      <Memo value={a}>
        <Child>
          <Nothing2 a={a} />
        </Child>
      </Memo>
    </>
  )
}

When a changes, Child renders, otherwise it bails out with previous render.

I outline the rational behind it in my blog, https://javascript.plainenglish.io/can-usememo-skip-a-child-render-94e61f5ad981

Devonne answered 28/8, 2021 at 0:25 Comment(5)
Great, simple explanation, thank you!Supersaturated
I use this tip every time, it saves a lot of bad rendering process. I am wondering if this is a good practice?Intervale
@ThomasDecaux, it's not popular approach if that's what you mean. React team designed the system with the mindset that they don't want us to over-think the engine, thus when it comes to the optimization, you just have to roll your own version. The pattern can be seen in quite a few libraries (including React itself), i believe React.memo even supports a third argument to make it more flexible. But in consumer code, you hardly see this pattern because you can just manually write these lines without the helper.Devonne
some guys told me we should never memo components, this means a wrong architecture. And they recommend me to use proxy pattern or lib such as zustand.Intervale
@ThomasDecaux, they are funny. If you ever checkout the source code of these library, ex. proxy or zustand, you'll see full of useMemo and useCallback used ;) I bet each library has at least five time usages of them. You can check yourself if you don't believe me.Devonne

© 2022 - 2024 — McMap. All rights reserved.