Search code examples
javascriptreactjsreact-hooksuse-statereact-memo

Why the props that are passed to memo are don't store the value


I am trying to optimize my React list rendering using the React memo feature of manual props comparison. I have generated a list of simple "toggle" buttons:

import React, { useState } from "react";
import "./styles.css";
import { Toggle } from "./Toggle";

export default function App() {
  const [list, setList] = useState({ a: true, b: true, c: true });
  const handleClick = x => {
    console.log(list);
    const currentValue = list[x];
    setList({ ...list, [x]: !currentValue });
  };

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      {Object.keys(list).map(x => (
        <Toggle key={x} isChecked={list[x]} name={x} onChange={handleClick} />
      ))}
    </div>
  );
}

This is the "toggle" button:

import React from "react";

const areEqual = (prevProps, nextProps) => {
  return prevProps.isChecked === nextProps.isChecked;
};

const ToggleComponent = ({ isChecked, name, onChange }) => {
  return (
    <>
      <h1>{isChecked ? "This is true" : "This is false"}</h1>
      <button onClick={() => onChange(name)}>{name}</button>
    </>
  );
};

export const Toggle = React.memo(ToggleComponent, areEqual);

My issue is that the list object actually doesn't store the expected value. Every time I click on the buttons I get the same, default one { a: true, b: true, c: true } (it is visible in the console.log of handleClick), but if I delete the areEqual function, everything works properly again and the list object is updated as it should be.

Code Sandbox example

Edit cool-sun-wq1f6

EDIT:

I saw that if I change the whole thing into an array and wrap every button into an object, the memo feature works as intended.

Code Sandbox example with array

Edit cool-sun-wq1f6


Solution

  • It is because the handleClick function is created and passed once to a Toggle Component. And handleClick's closure contains the old list value, so whenever the old value change it doesn't get updated.

    The easiest fix is to benefit from the second signature of the state updater: a function that accepts in parameter the old state value.

    So whenever it is called, react will pass the old state value to it.

    const handleClick = x => {
      setList(old => ({ ...old, [x]: !old[x] }));
    };
    

    You also need to memoize the handleClick function, because it is recreated at each render of the component holding the state:

    const handleClick = React.useCallback(x => {
      setList(old => ({ ...old, [x]: !old[x] }));
    }, [setList]);
    

    Here is working codesandbox