skip to Main Content

I have a list with a large amount of items inside. In order to improve rendering performance I decided to memoize single items. This is the code I used:

Parent component

const randomList = [];
for (let i = 0; i < 1000; i++) {
  const randomInteger = Math.floor(Math.random() * 100);
  randomList.push({ id: randomInteger, status: "" });
}

const List = () => {
  const [list, setList] = useState(randomList);

  const handleClick = (e) => {
     const position = e.currentTarget.id;
     const newList = list.map((item, idx) =>
      `${idx}` === position ? { ...item, status: "red" } : item
     );
     setList(newList);
  };

  return (
     <div>
        {list.map(({ id, status }, idx) => {
           return <Item id={id} idx={idx} status={status} onClick={handleClick} />;
        })}
     </div>
  );
};

Item component

export const ChipTermStyled = styled(({ ...props }) => <Chip {...props} />)(
  ({ status = "" }) => {
    return {
      backgroundColor: status,
    };
  }
);

const Item = ({ id, idx, status, onClick }) => {
  console.log(`item-${idx}`);
  return (
    <ChipTermStyled
      id={`${idx}`}
      key={`${id}-${idx}`}
      label={`${id}-${idx}`}
      status={status}
      onClick={onClick}
    />
  );
};

const arePropsEqual = (prevProps, newProps) =>
  prevProps.id === newProps.id &&
  prevProps.idx === newProps.idx &&
  prevProps.status === newProps.status;

export default memo(Item, arePropsEqual);

This should render Item only when id, idx or status change. And it does the job. But! When I update the parent component list, it doesn’t keep the state between item updates. See this:

enter image description here

I check the list before and after on the first click and they appear ok, but on the second click, the list lost all the previous item statuses.

Why can this be happening? What I’m doing wrong?

2

Answers


  1. Chosen as BEST ANSWER

    The problem was with how the state is being updated. For anyone that is facing this issue, instead of updating the state like this:

     const handleClick = (e) => {
        const position = e.currentTarget.id;
        const newList = list.map((item, idx) =>
         `${idx}` === position ? { ...item, status: "red" } : item
        );
        setList(newList);
     };
    

    You have to update it in the previous value of the state:

     const handleClick = (e) => {
     const position = e.currentTarget.id;
     setList(prevList => 
        prevList.map((item, idx) =>
          ${`idx`} === position 
             ? { ...item, status: "red" } 
             : item
        ));
     };
    

  2. Your problem is caused by a combination of the memoization, and the way you update the list.

    The handleClick function is re-created on each render, and contains the updated list:

    const handleClick = (e) => {
      const position = e.currentTarget.id;
      const newList = list.map((item, idx) =>
        `${idx}` === position ? { ...item, status: "red" } : item
      );
      setList(newList);
    };
    

    However, your memoization ignores changes to handleClick, and all your components, except the last updated hold a reference to stale version of handleClick, which contains the original version of the list in which nothing is marked:

    const arePropsEqual = (prevProps, newProps) =>
      prevProps.id === newProps.id &&
      prevProps.idx === newProps.idx &&
      prevProps.status === newProps.status;
    
    export default memo(Item, arePropsEqual);
    

    To solve that problem, and still have memoization on the Item component, wrap handleClick with useCallback, and use a function to update the state instead of using list directly:

    const handleClick = useCallback(e => {
      const position = e.currentTarget.id;
      setList(prevList => 
        prevList.map((item, idx) =>
          ${`idx`} === position 
            ? { ...item, status: "red" } 
            : item
      ));
    }, []);
    

    Now all the properties are memoized, and you don’t need arePropsEqual anymore, so just wrap your component with memo directly:

    const Item = memo(({ id, idx, status, onClick }) => {
      const key = `${id}-${idx}`;
      
      return (
        <div
          id={idx}
          className="item"
          style={{ background: status }}
          onClick={onClick}
        >{key}</div>
      );
    });
    

    Demo:

    const { useState, useCallback, memo } = React;
    
    const Item = memo(({ id, idx, status, onClick }) => {
      const key = `${id}-${idx}`;
      
      return (
        <div
          id={idx}
          className="item"
          style={{ background: status }}
          onClick={onClick}
        >{key}</div>
      );
    });
    
    const generateList = () => {
      const randomList = [];
      
      for (let i = 0; i < 1000; i++) {
        const randomInteger = Math.floor(Math.random() * 100);
        randomList.push({ id: randomInteger, status: "" });
      }
      
      return randomList;
    };
    
    const List = () => {
      const [list, setList] = useState(generateList);
    
      const handleClick = useCallback(e => {
         const position = e.currentTarget.id;
         setList(prevList => 
          prevList.map((item, idx) =>
            `${idx}` === position 
              ? { ...item, status: "red" } 
              : item
         ));
      }, []);
    
      return (
         <div>
            {list.map(({ id, status }, idx) => (
              <Item 
                key={`${id}-${idx}`}
                id={id} 
                idx={idx} 
                status={status} 
                onClick={handleClick} />
            ))}
         </div>
      );
    };
    
    ReactDOM
      .createRoot(root)
      .render(<List />);
    .item {
      display: inline-block;
      padding: 0.25em;
      margin: 0.25em;
      border: 1px solid grey;
      border-radius: 25%;
    }
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    
    <div id="root"></div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search