Evgeny
Evgeny

Reputation: 641

Avoid re-rendering whole list in React

I have a trivial list of items, which can update themselves. Update of one item triggers a re-render of all items. I provide unique keys for items, I'd expect React will skip the update of unchanged items. Even when App recreates items and handleItemUpdate function.

What is wrong?

The Codepen example: https://codepen.io/enepom/pen/VwKdxZN
Tap on one item prints 3 item renders in a console, not one.

Item component:

const Item = React.memo(({ id, count, onUpdate }) => {
  console.log('> ITEM RENDER', id);

  const handleClick = () => {
    onUpdate(id, count + 1);
  };

  return (
    <li onClick={handleClick}>{id}: {count}</li>
  );
});

App component:

const App = () => {
  const [items, setItems] = React.useState([]);

  React.useEffect(() => {
    setItems([
      { id: 'id1', count: 7 },
      { id: 'id2', count: 8 },
      { id: 'id3', count: 9 },
    ]);
  }, []);

  const handleItemUpdate = React.useCallback((itemId, count) => {
    const itemIndex = items.findIndex(item => item.id === itemId);
    if (itemIndex > -1) {
      const itemsCopy = items.slice();
      itemsCopy[itemIndex].count = count;

      setItems(itemsCopy);
    }
  }, [items, setItems]);

  return (
    <ul>
      {items.map(item => (
        <Item key={item.id} id={item.id} count={item.count} onUpdate={handleItemUpdate} />
      ))}
    </ul>
  );
};

Upvotes: 0

Views: 877

Answers (4)

Evgeny
Evgeny

Reputation: 641

The problem was in dependency on items in handleItemUpdate.

This code works as expected:

const handleItemUpdate = React.useCallback((itemId, count) => {
  setItems(prevItems => prevItems.map(item =>
    item.id === itemId
      ? { ...item, count }
      : item
  ));
}, [setItems]);

Codepen: https://codepen.io/enepom/pen/qBaKYye

Upvotes: 1

Sonia Aguilar
Sonia Aguilar

Reputation: 171

This is a way to avoid unnecessary renders:

Add equality function to React.memo:

const Item = React.memo(({ id, count, onUpdate,items }) => {
  console.log('> ITEM RENDER', id);

  const handleClick = () => {
    onUpdate(id, count + 1,items);
  };

  return (
    <li onClick={handleClick}>{id}: {count}</li>
  );
}, (prev, next) => prev.id===next.id && prev.count===next.count);


const App = () => {
  const [items, setItems] = React.useState([]);

  React.useEffect(() => {
    setItems([
      { id: 'id1', count: 7 },
      { id: 'id2', count: 8 },
      { id: 'id3', count: 9 },
    ]);
  }, []);

  const handleItemUpdate = React.useCallback((itemId, count,items) => {
    const itemIndex = items.findIndex(item => item.id === itemId);
    if (itemIndex > -1) {
      const itemsCopy = items.slice();
      itemsCopy[itemIndex].count = count;

      setItems(itemsCopy);
    }
  }, []);

  return (
    <ul>
      {items.map(item => (
        <Item key={item.id} id={item.id} count={item.count} onUpdate={handleItemUpdate} items={items}/>
      ))}
    </ul>
  );
};
ReactDOM.render(<App />, document.getElementById("root"));

Upvotes: 0

Sam
Sam

Reputation: 1239

Here is one way you can do it. React.memo does a shallow comparison, as you are passing an object it might different by reference even though values are the same, you need to implement arePropsEqual with your custom logic.

const Item = ({ id, count, onUpdate }) => {
  console.log('> ITEM RENDER', id);

  const handleClick = () => {
    onUpdate(id, count + 1);
  };

  return (
    <li onClick={handleClick}>{id}: {count}</li>
  );
};
const arePropsEqual =(next,prev)=>{
  return next.id ===prev.id &&  next.count ===prev.count
}
const MemoItem = React.memo(Item,arePropsEqual);


const App = () => {
  const [items, setItems] = React.useState([]);

  React.useEffect(() => {
    setItems([
      { id: 'id1', count: 7 },
      { id: 'id2', count: 8 },
      { id: 'id3', count: 9 },
    ]);
  }, []);

  const handleItemUpdate = React.useCallback((itemId, count) => {
    const itemIndex = items.findIndex(item => item.id === itemId);
    if (itemIndex > -1) {
      const itemsCopy = items.slice();
      itemsCopy[itemIndex].count = count;

      setItems(itemsCopy);
    }
  }, [items, setItems]);

  return (
    <ul>
      {items.map(item => (
        <MemoItem key={item.id} id={item.id} count={item.count} onUpdate={handleItemUpdate} />
      ))}
    </ul>
  );
};

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

Upvotes: 0

Sonia Aguilar
Sonia Aguilar

Reputation: 171

Any time you update the items, the handleItemUpdate is recalculated becasue the items is changed , and in the useCallback dependencies array you have :

 [items, setItems]

So, each Item is rendered again as one property (onUpdate) has changed.

Upvotes: 1

Related Questions