Pete
Pete

Reputation: 3451

Problem with re-renders in React with useCallback and useMemo

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>
  );
});

Upvotes: 2

Views: 1606

Answers (2)

adel
adel

Reputation: 3497

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>
  );
});

Upvotes: 2

ray
ray

Reputation: 27245

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>
  );
};

Upvotes: 2

Related Questions