Praveen Kumar Yadav
Praveen Kumar Yadav

Reputation: 103

useCallback is not working when using with child component of same type

So I have a parent Component and a child component. And I use the child component twice in my parent component. I pass them two different state values as props and two different events as props. I have tried to memoize both the callbacks , but both the child are re-rendered even if one child callback is triggred. Why is useCallback not working.

Parent Component:

import { useState, useCallback, useEffect, useMemo } from 'react';
    import './App.css'
    import List from "./components/list";
    import LocalList from "./components/localList";

function App() {
  const itemsToBuy = [
    'Baby Shoes',
    'Grinder',
    'Car'
  ]
 
 
  const [buyList, updateBuyList] = useState(itemsToBuy);
  const [sellList, updateSellList] = useState([
    'Bed',
    'Sofa'
  ]);
  
  /** code to check the re-rendering of the componnet  */
  useEffect(() => {
    console.log(`parent is being rendered`)
  })

  /**trying to update the state from internal method to be passed as props */

  const updateBuyClick = useCallback(val => {
    updateBuyList(prev => [...prev, val])
  }, [buyList])

  const updateSellClick = useCallback(val => {
    console.log('memo of sell is called')
    updateSellList(prev => [...prev, val])
  }, [sellList])


  return (
    <>
      <div className='container'>
        <div>
          <h1>Items To Buy</h1>
          <List itemsArray={buyList} onUpdateClick={updateBuyClick} buttonText='Add Items to Buy' idx={'list One'}></List>
        </div>
        <div>
          <h1>Items to Sell</h1>
          <List itemsArray={sellList} onUpdateClick={updateSellClick} buttonText='Add Items to Sell' idx={'list Two '}></List>
        </div>
        {/* <div>
          <h1>List that is not re-rendere</h1>
          <LocalList buttonText='Add Items to LocalList' idx={'list3 '}></LocalList>
        </div> */}
      </div>
    </>
  );
}

export default App;

Child Component:

import { useState , useEffect} from "react";
import './list.css'

function List({ itemsArray = [], buttonText, onUpdateClick, idx }) {
    let currentSell = '';

    useEffect(() => {
        console.log(`${idx} is being rendered`)
    })
    const updateCurrentSell = (val) => {
        currentSell = val;
    }
    return (
        <>
            <ul>
                {itemsArray.map((value, index) => {
                    return <li key={index}>{value}</li>
                })}
            </ul>

            <div>

                <input type='text' onChange={(e) => { updateCurrentSell(e.target.value) }}></input>
                <button onClick={() => { onUpdateClick(currentSell) }}>{buttonText}</button>
            </div>
        </>
    )
}

export default List;

Upvotes: 2

Views: 2796

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1075587

There are two reasons that's not working:

  1. You're telling useCallback to throw away the stored copy of your function when the buyList or sellList changes by including those in your dependencies array. You don't need those dependencies, because you're (correctly) using the callback version of the state setters. So you aren't using buyList or sellList in the callbacks. Just remove them from the arrays.

    const updateBuyClick = useCallback(val => {
      updateBuyList(prev => [...prev, val])
    }, [])
    // ^^−−− empty
    
    const updateSellClick = useCallback(val => {
      console.log('memo of sell is called')
      updateSellList(prev => [...prev, val])
    }, [])
    // ^^−−− empty
    
  2. useCallback only does half the necessary work: making sure the functions don't change unnecessarily. But your List component has to do the other half of the work: not re-rendering if its props don't change. With a function component, you do that with React.memo:

    const List = React.memo(function List({ itemsArray = [], buttonText, onUpdateClick, idx }) {
        // ...
    });
    

    React.memo memoizes the component and reuses its last rendering if its props don't change. (You can customize that by providing a callback as its second argument, see the documentation for details.)

Between those two changes, you'll see only the appropriate instances of List re-render when things change.

Live Example:

const { useState, useCallback, useEffect, useMemo } = React;

function App() {
    const itemsToBuy = [
        "Baby Shoes",
        "Grinder",
        "Car"
    ];

    const [buyList, updateBuyList] = useState(itemsToBuy);
    const [sellList, updateSellList] = useState([
        "Bed",
        "Sofa"
    ]);

    // *** Note: No need for this to be in `useEffect`
    console.log(`parent is being rendered`)

    const updateBuyClick = useCallback(val => {
        updateBuyList(prev => [...prev, val]);
    }, []);

    const updateSellClick = useCallback(val => {
        updateSellList(prev => [...prev, val])
    }, []);

    return (
        <div className="container">
            <div>
                <h1>Items To Buy</h1>
                <List itemsArray={buyList} onUpdateClick={updateBuyClick} buttonText="Add Items to Buy" idx={"list One"}></List>
            </div>
            <div>
                <h1>Items to Sell</h1>
                <List itemsArray={sellList} onUpdateClick={updateSellClick} buttonText="Add Items to Sell" idx={"list Two "}></List>
            </div>
        </div>
    );
}

const List = React.memo(function List({ itemsArray = [], buttonText, onUpdateClick, idx }) {
    // *** `currentSell` stuff should be in state, not a local variable
    const [currentSell, setCurrentSell] = useState("");

    console.log(`${idx} is being rendered`);

    return (  // <>...</> is fine, I had to change it because the
            // version of Babel Stack Snippets use is out of date
        <React.Fragment>
            <ul>
                {itemsArray.map((value, index) => {
                    return <li key={index}>{value}</li>
                })}
            </ul>

            <div>
                <input type="text" onChange={(e) => { setCurrentSell(e.target.value); }}></input>
                <button onClick={() => { onUpdateClick(currentSell); }}>{buttonText}</button>
            </div>
        </React.Fragment>
    );
});

ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

Upvotes: 1

Related Questions