Search code examples
javascriptreactjsreact-hooksusecallback

useCallback dependency array defeats the purpose of preventing re-renders when updating an array


The following React app displays a list of fruits. Each Fruit has a "add to fav" button which uses a addFav callback to add said fruit to a list of favourites in the parent. Passing in the handleAddFav callback causes unnecessary re-renders, so I wrapped it in a useCallback and Fruit in memo.

However the useCallback demands to have favs in its dependency array which causes the handleAddFav to be re-computed every time its called. This defeats the purpose of using useCallback to stop re-renders because now each Fruit re-renders every time you add a favourite. How can I solve this?

import { useState, memo, useCallback } from "react";
import "./styles.css";

const Fruit = memo(({title, id, addFav}) => {
  console.log(title, 'rendered')
  return (
    <div>
      <div>{title}</div>
      <button onClick={() => addFav(title, id)}>add fav</button>
    </div>
  )
})

export default function App() {
  const [favs, setFavs] = useState([])
  const data = [{title: 'apple', id: '1'}, {title:'orange', id:'2'}
  , {title:'banana', id:'3'}]

  const handleAddFav = useCallback((title, id) => {
    setFavs([...favs, {title, id}])
  }, [favs])
  return (
    <div className="App">
      <h1>Testing useCallback that sets an array</h1>
      <h2>Favorites</h2>
      <button onClick={() => setFavs([])}>clear</button>
      {
        favs.map(({title, id}, i) => <span key={id + i}>{title}</span>)
      }
      {
        data.map(({title, id }) => (
          <Fruit key={id} title={title} id={id} addFav={handleAddFav}/>
        ))
      }
    </div>
  );
}

Solution

  • One way is to use the function version of setFavs instead, so it doesn't depend on an outer variable.

    const handleAddFav = useCallback((title, id) => {
      setFavs(favs => [...favs, {title, id}])
    }, [])
    

    For the more general situation - even if you did have a value that had to be re-computed, using useCallback could still reduce re-renders for the cases in which other values in the component change, but not the computed value. For example

    const TheComponent = () => {
      const [toggled, setToggled] = useState(false);
      const [num, setNum] = useState(5);
      const cb = useCallback(() => {
        // imagine that this does something that depends on num
      }, [num]);
    

    If cb is passed down, even though it depends on num, useCallback will still prevent child re-renders in the case where only toggled gets changed, and num stays the same.