Pete
Pete

Reputation: 3451

useCallback in React seems to be not letting me update state in parent

I've created a simple example of how useCallback is not allowing me to preserve state changes. When I remove the useCallback, the counters that I store in state update as expected, but adding useCallback (which I was hoping would keep rerenders of all speaker items to not re-render) keeps resetting my state back to the original (0,0,0).

The problem code is here in codesandbox:

https://codesandbox.io/s/flamboyant-shaw-2wtqj?file=/pages/index.js

and here is the actual simple one file example

import React, { useState, memo, useCallback } from 'react';

const Speaker = memo(({ speaker, speakerClick }) => {
  console.log(speaker.id)
  return (
    <div>
      <span
        onClick={() => {
          speakerClick(speaker.id);
        }}
        src={`speakerimages/Speaker-${speaker.id}.jpg`}
        width={100}
      >{speaker.id} {speaker.name}</span>
      &nbsp;&nbsp;
      <span className="fa fa-star ">&nbsp;&nbsp;{speaker.clickCount}</span>
      &nbsp;
    </div>
  );
});

function SpeakerList({ speakers, setSpeakers }) {
  return (
    <div>
      {speakers.map((speaker) => {
        return (
          <Speaker
            speaker={speaker}
            speakerClick={useCallback((id) => {
              const speakersNew = speakers.map((speaker) => {
                return speaker.id === id
                    ? { ...speaker, clickCount: speaker.clickCount + 1 }
                    : speaker;
              });
              setSpeakers(speakersNew);
            },[])}
            key={speaker.id}
          />
        );
      })}
    </div>
  );
}

//
const App = () => {
  const speakersArray = [
    { id: 1124, name: 'aaa', clickCount: 0 },
    { id: 1530, name: 'bbb', clickCount: 0 },
    { id: 10803, name: 'ccc', clickCount: 0 },
  ];

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

  return (
    <div>
      <h1>Speaker List</h1>
      <SpeakerList speakers={speakers} setSpeakers={setSpeakers}></SpeakerList>
    </div>
  );
};
export default App;

Upvotes: 0

Views: 729

Answers (1)

buzatto
buzatto

Reputation: 10382

first, you can only use a hook at component body, you can't wrap it at speakerClick props function declaration. second, useCallback will keep the original speakers object reference, which will be a stale value. To solve this, you can use setSpeakers passing a callback instead, where your function will be called with the current speakers state:

function SpeakerList({ speakers, setSpeakers }) {
  const speakerClick = useCallback(
    (id) => {
      // passing a callback avoid using a stale object reference
      setSpeakers((speakers) => {
        return speakers.map((speaker) => {
          return speaker.id === id
            ? { ...speaker, clickCount: speaker.clickCount + 1 }
            : speaker;
        });
      });
    },
    [setSpeakers] // you can add setSpeakers as dependency since it'll remain the same
  );

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

Upvotes: 1

Related Questions