React.memo isn't working - what am I missing?
Asked Answered
N

5

16

I'm in the process of refactoring some of our components so I'm trying to incorporate memoization as some components may re-render with the same values (for example, hotlinked image URLs unless they are the same).

I have a simple component:

const CardHeader = props => {
    // img is a stringand showAvatar is a boolean but it's always true
    const { ..., showAvatar, img } = props;

    return (
        <CardHeader>
            <ListItem>
                // AvatarImage shouldn't re-render if img is the same as previous
                {showAvatar && <AvatarImage img={img} />
            </ListItem>
        </CardHeader>
    );
}

And then the AvatarImage:

const AvatarImage = React.memo(props => {
   console.log("why is this still re-rendering when the img value hasn't changed?");
   const { img } = props;

   return (
        <ListItemAvatar>
            {img ?
                <Avatar src={img} />    
                :
                <Avatar>
                    Some initials
                </Avatar>
            }
        </ListItemAvatar>
    );
});

I have also tried passing in second argument of memo:

(prevProps, nextProps) => {
    return true; // Don't re-render!
}

But the console.log still shows every time. I'm obviously missing something here or don't quite understand how this works. This component is a few levels down, but it passes in the img if it's available every time so I'd expect it to know that if the img was passed in the previous render and it's the same it knows not to re-render it again but for some reason it does?

Thanks all. It's much appreciated.

Nika answered 27/5, 2020 at 11:22 Comment(8)
1. Is img a string? 2. Is there a chance showAvatar is changing in between those renders?Schwaben
Yep, img is a string ("https://.........jpg"). Show avatar is always true in this case. Sorry, I should have mentioned that. I will update.Nika
Could you show how CardHeader is used and how the image values are passed on to it?Bend
It looks like information missing, the bug isn't in front of us right now. If you can reproduce with codesandbox.io or a fiddle it would be greatSchwaben
@Nika Have you ever found the reason? Facing the same issue right now. All props stay the same, React.memo re-renders all the time anyway.Divider
not sure about the OP, but what was wrong on my end was that I stupidly defined the memoized component in a non-memoized component 🙈Softfinned
@JacobGoh Thank you so much for sharing! You may consider it as a stupid mistake but defining memoized components outside of non-memo components isn't obvious to beginners in React, like me. I spent almost an hour searching for a solution to my problem and only realised that what I was doing was wrong once I read your comment.Wunder
Well, if you could point to some doc or example of what would be to define a non-memoized component in a memoized component, I'd be grateful.Chainplate
D
3

Do you have StrictMode enabled? That will cause a component memoized with React.memo to render twice.

More information:

Diarrhea answered 8/10, 2021 at 19:22 Comment(0)
I
2

I had the same issue and spent a lot of time figuring it out. I also tried this:

export default React.memo(Component, () => {console.log("Memo check"); return true;}

Which should never refresh the wrapped Component, and log "Memo check" in the console.

Result: it wasn't ever displaying anything! This was an indicator that the Component was somehow always rendered for the first time, hence memoization could not work.

The root of these kinds of issues is usually located up in the component hierarchy.

Solution:

In my case, in one of the parent components, I had something like this:

const SubComponent = () => (
    <Component /> // it was actually a parent component of the Component, so less obvious to see..
)
...
return (<div>
    ...
    <SubComponent/>
    ...
</div>);

This was actually defining a new "SubComponent" at every refresh of the parent component and displaying it for the "first time".

I simply changed this:

<SubComponent/>

to this:

{SubComponent()}

and it resolved the problem!

Indeed, this is different than the previous code since it is simply a call to a function returning the JSX Component , while the first one was defining a new SubComponent component every time.

Hope this helps.

Intervocalic answered 24/4, 2023 at 10:26 Comment(0)
C
0

Well it is either showAvatar is not always true or CardHeader ListItem component magically decides whether show children or not

Example

const { useState, useEffect, memo, createContext, useContext } = React;

const getAvatars = () => Promise.resolve([
{
  src: 'https://i.picsum.photos/id/614/50/50.jpg'
},
{
  src: 'https://i.picsum.photos/id/613/50/50.jpg'
}
])

const Avatar = ({src}) => {
console.log('avatar render');
  return <img src={src} alt="avatar"/>
}

const MemoAvatarToggle = memo(({src}) => {
console.log('memo avatar with \'expression &&\' render');
  return <div>
  {src ? <img src={src} alt="avatar"/> : <div>Test </div>}
  </div>
})

const CardHeader = ({children}) => {
  const luck = Boolean(Math.floor(Math.random() * 1.7));
  
  
  
  return <div>
    {luck && children}
  </div>
}

const ListItem = ({children}) => {
  return <div>
    {children}
  </div>
}

const ShowAvatarContext = createContext()

const App = (props) => {
  const [avatars, setAvatars] = useState([]);
  const [toggle, setToggle] = useState(false);
  const [showAvatar, setShowAvatar] = useContext(ShowAvatarContext);
  
  useEffect(() => {
    let isUnmounted = false;
    let handle = null;
    
    setTimeout(() => {
      if(isUnmounted) {
        return;
      }
      setShowAvatar(true);
    }, 500);
    
    getAvatars()
      .then(avatars => {
        if(isUnmounted) {
          return;
        }
        
        setAvatars(avatars)
      })
    
    const toggle = () => {
      setToggle(prev => !prev);
      handle = setTimeout(toggle, 1000);
      //setShowAvatar(prev => !prev);
    }
    
    handle = setTimeout(toggle, 1000);
    
    return () => {
      isUnmounted = true;
      clearTimeout(handle);
    }
      
  }, []);
 
  return <div>
    <CardHeader>
      <ListItem>
        {showAvatar && avatars.map((avatar, index) => <MemoAvatarToggle key={index} src={avatar.src}/>)}
      </ListItem>
    </CardHeader>
    {toggle ? 1 : 0} 
  </div>
}

const ShowAvatarProvider = ({children}) => {
  const state = useState(false);
  
  return <ShowAvatarContext.Provider value={state}>
      {children}
    </ShowAvatarContext.Provider>
}

ReactDOM.render(
    <ShowAvatarProvider>
        <App/>
    </ShowAvatarProvider>,
    document.getElementById('root')
  );
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<div id="root"></div>
Clavicembalo answered 27/5, 2020 at 12:46 Comment(0)
L
0

memo will not block re-render if the component is actually referenced the changing props or functions.

In your scenario your AvatarImage referenced img, in this case if parent's state's img is changed, then your component will be re-rendered. Alternatively, if your parent is just changed other props instead of img, then the AvatarImage will NOT be re-rendered.

Alternatively, if any props but you didn't add memo to AvatarImage, then AvatarImage will be re-rendered for each of parent's state updated.

Launderette answered 1/9, 2022 at 5:43 Comment(0)
T
-1

You need to memorized img props too.

const CardHeader = props => {
  const { showAvatar, img } = props;
  const updatedIMG = React.useMemo(() => img, []);
  return (
    <CardHeader>
        <ListItem>
           {showAvatar && <AvatarImage img={updatedIMG} />
        </ListItem>
    </CardHeader>
  );
}

Above one would work

Tabatha answered 28/12, 2022 at 7:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.