2

I've got a fairly simple example of a component (Hello.js) that renders three components, each with a different id (Speaker.js). I have a clickFunction that I pass back from the Speaker.js. I would think that using React.memo and React.useCallback would stop all three from re-rendering when only one changes, but sadly, you can see from the console.log in Speaker.js, clicking any of the three buttons causes all three to render.

Here is the problem example on stackblitz:

https://stackblitz.com/edit/react-dmclqm

Hello.js

import React, { useCallback, useState } from "react";

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 = useCallback((speakerIdClicked) => {
    var speakersArrayUpdated = speakers.map((rec) => {
      if (rec.id === speakerIdClicked) {
        rec.favorite = !rec.favorite;
      }
      return rec;
    });
    setSpeakers(speakersArrayUpdated);
  },[speakers]);

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

Speaker.js

import React from "react";

export default React.memo(({ speaker, clickFunction }) => {
  console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
  return (
    <button
      onClick={() => {
        clickFunction(speaker.id);
      }}
    >
      {speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
    </button>
  );
});

2 Answers 2

2

because when you fire clickFunction it update speakers wich cause the recreating of this functions, to solve this you need to remove speakers from clickFunction dependencies and accessing it from setState callback. here the solution :

import React, { useCallback, useState,useEffect } from "react";

import Speaker from "./Speaker";

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

  const clickFunction = useCallback((speakerIdClicked) => {
    setSpeakers(currentState=>currentState.map((rec) => {
      if (rec.id === speakerIdClicked) {
        rec.favorite = !rec.favorite;
       return {...rec};
      }
     return rec
    }));
  },[]);
  useEffect(()=>{
    console.log("render")
  })

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

and for speaker component:

import React from "react";

export default React.memo(({ speaker, clickFunction }) => {
  return (
    <button
      onClick={() => {
        clickFunction(speaker.id);
      }}
    >
      {speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
    </button>
  );
});
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks @adel. Appreciate you taking the extra time to go back and improve the answer. I was struggling with how you made it work the first time, but second was easy since it was just the clickFunction change.
this was really a leadup to my next question which I couldn't ask until I got this one working. If you have time to look at it, it's almost identical. stackoverflow.com/questions/63200464/…
2

Upon further reflection, I think my answer may not be entirely correct: without the [speakers] dependency this won't work as intended.

Two things:

  1. The [speakers] dependency passed to useCallback causes the function to get recreated every time speakers changes, and because the callback itself calls setSpeakers, it will get recreated on every render.

  2. If you fix #1, the Speaker components won't re-render at all, because they're receiving the same speaker prop. The fact that speaker.favorite has changed doesn't trigger a re-render because speaker is still the same object. To fix this, have your click function return a copy of rec with favorite flipped instead of just toggling it in the existing object:

import React, { useCallback, useState } from "react";

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 = useCallback((speakerIdClicked) => {
    var speakersArrayUpdated = speakers.map((rec) => {
      if (rec.id === speakerIdClicked) {
        return { ...rec, favorite: !rec.favorite }; // <= return a copy of rec
      }
      return rec;
    });
    setSpeakers(speakersArrayUpdated);
  }, []); // <= remove speakers dependency

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

3 Comments

thanks. big help. I still get the warning which I don't understand: Line 22:5: React Hook useCallback has a missing dependency: 'speakers'. Either include it or remove the dependency array react-hooks/exhaustive-deps overrideMethod @ react_devtools_backend.js:2273 printWarnings @ webpackHotDevClient.js:138 handleWarnings @ webpackHotDevClient.js:143 push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage @ webpackHotDevClient.js:210.
Upon further reflection, I think my answer may not be entirely correct: without the [speakers] dependency this won't work as intended.
check my answer you can access current state from setState callback so the useCallback doesn't relay on it, which can be removed from dependencies.

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.