2

I have a trivial example I built that shows the function passed into the second parameter of React.memo seems to not get the previous property value as I would expect. This example is a simple list of speakers (objects) that are rendered as buttons. The onClick event on the button caused passed up to the parent component, state changes in the parent, and a new render should show just the updated component.

However, regardless of the previous state, the passed in previous is always the same as the new state.

The example is at this URL: https://stackblitz.com/edit/react-upe9vu

In the component Hello.js the current import is for ./Speaker however, if you change that to ./SpeakerWithMemo as shown in the line above, the button click fails to update the "favorite" status.

I would expect that in SpeakerWithMemo.js that the console.log at line 12 would show the prevProps.speaker.favorite different then the nextProps.speaker.favorite.

The relevant code is below:

Hello.js

import React, { useState } from "react";

// NO UPDATE HAPPENS ON BUTTON CLICK.
//import Speaker from "./SpeakerWithMemo";

// UPDATE HAPPENS AS EXPECTED ON BUTTON CLICK
import Speaker from "./Speaker";

export default () => {
  const speakersArray = [
    { name: "Crockford", id: 101, favorite: true },
    { name: "Gupta", id: 102, favorite: false },
    { name: "Ailes", id: 103, favorite: true },
  ];

  const [speakers, setSpeakers] = useState(speakersArray);

  const clickFunction = (speakerIdClicked) => {
    var speakersArrayUpdated = speakers.map((rec) => {
      if (rec.id === speakerIdClicked) {
        rec.favorite = !rec.favorite;
      }
      return rec;
    });
    setSpeakers(speakersArrayUpdated);
  };

  return (
    <div>
      {speakers.map((rec) => {
        return (
          <Speaker
            speaker={rec}
            key={rec.id}
            clickFunction={() => {
              clickFunction(rec.id);
            }}
          ></Speaker>
        );
      })}
    </div>
  );
};

SpeakerWithMemo.js

import React from "react";

export default React.memo(
  ({ speaker, clickFunction }) => {
    console.log(`Rendering Speaker ${speaker.name} ${speaker.favorite}`);
    return (
      <button onClick={clickFunction}>
        With Memo {speaker.name} {speaker.id}{" "}
        {speaker.favorite === true ? "true" : "false"}
      </button>
    );
  },
  (prevProps, nextProps) => {
    console.log(
      `memo: ${prevProps.speaker.favorite === nextProps.speaker.favorite} ${
        prevProps.speaker.name
      } fav: prev: ${prevProps.speaker.favorite} next: ${
        nextProps.speaker.favorite
      } `
    );
    return prevProps.speaker.favorite === nextProps.speaker.favorite;
  }
);

1 Answer 1

1

The memo isn't working quite as expected because you've a state mutation. By not returning a new object reference react bails on rerendering, i.e. the props.speaker object reference never changes so react assumes it is irrelevant to check more deeply.

const clickFunction = (speakerIdClicked) => {
  var speakersArrayUpdated = speakers.map((rec) => {
    if (rec.id === speakerIdClicked) {
      rec.favorite = !rec.favorite; // <-- Mutates the object you try to memoize
    }
    return rec;
  });
  setSpeakers(speakersArrayUpdated);
};

For the element you are trying to update you need to return a new object reference. Also, when queuing toggle state updates like this you should use a functional state update.

const clickFunction = speakerIdClicked => {
  setSpeakers(speakers =>
    speakers.map(rec =>
      rec.id === speakerIdClicked
        ? {
            ...rec, // <-- shallow copy existing value
            favorite: !rec.favorite // <-- update field/property
          }
        : rec
    )
  );
};

https://react-eyvskj.stackblitz.io

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks @DrewReese (again). Total rookie move on my part. I know better than to mutate state. Back when I was doing redux, there was some middleware that threw an error when I did that. Is there anything now that helps to avoid problems like this at dev time?
@Pete I think there are some immutable libraries (I've heard of immutablejs), but I think it's mostly good coding discipline and attention to detail. IDK, after a while you just sort of get a good sense where common mistakes can sneak in bite you.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.