Home > Back-end >  Getting maximum update depth exceeded even when using useCallback
Getting maximum update depth exceeded even when using useCallback

Time:11-09

I have this piece of code, I am still getting maximum update depth exceeded even though I have memoized my prop function. Why am I getting this ? And how should i fix it ? I am trying to dynamically create a list of child components to take some input in. The idea is to be able to add or remove said child components from the parent one and then do some processing on the user inputs

const RatingBracket = (props) => {
    const [range, setRange] = useState([])
    const [prices, setPrices] = useState({})

    const {target} = props

    const updatePrice = useCallback((selectedRating, price) => {
        const new_prices = { ...prices, [selectedRating]: price }
        setPrices(new_prices)
      }, [prices]);

    useEffect(()=>{
        if(target){
            const new_range = [
                Number(target)   1,
                Number(target) - 1
            ]
            setRange(new_range)
            console.log(new_range)
        }
    }, [target])

    return (
        <div className='rating-bracket-container'>
            {target}
            Player ratings to try
            {range.map(rating=>(
                // <></>
                <InputRow target={rating} updatePrice={updatePrice}/>
            ))}
        </div>
    )
}

const InputRow = (props) => {

    const {updatePrice, target} = props;
    const [rating, setRating] = useState(target)
    const [price, setPrice] = useState(0)

    useEffect(()=>{
        updatePrice({rating,price})
    }, [rating, price, updatePrice])

    return (
        <div className="input-row-container">
            Rating:
            <input value={rating} onChange={(e)=>setRating(e.target.value)} type="number"/>
            Price:
            <input value={price} onChange={(e)=>setPrice(e.target.value)} type="number"/>
        </div>
    )
}

CodePudding user response:

Use a function in the onChange instead of just your setters.



const onPriceChange = (value) => { 
   setPrice(value)
   updatePrice (rating, value)
}

const onRatingChange = (value) => {
setRating(value)
updatePrice (value, price)
}

<input value={rating} onChange={(e)=>onRatingChange(e.target.value)} type="number"/>
           

CodePudding user response:

So it turns out the problem is exactly as was already conjectured in the comments (before we could see InputRow).

I'll first explain why you were getting infinite rerenders, then show you a very simple way to adjust your code to avoid this.

The key bits of code that give rise to the problem are as follows.

1 - the useCallback that defines updatePrice in the parent component:

const updatePrice = useCallback((selectedRating, price) => {
  const new_prices = { ...prices, [selectedRating]: price }
  setPrices(new_prices)
}, [prices]);

2 - the passing of updatePrice to the child as a prop:

<InputRow target={rating} updatePrice={updatePrice}/>

3 - the useEffect in the child that calls this function (note that this runs after the first render, and after every subsequent render that changes any of the dependencies, one of which is updatePrice):

useEffect(()=>{
    updatePrice({rating,price})
}, [rating, price, updatePrice])

So the following things happen when your parent component is first rendered (I'll talk about it as if there is just one InputRow child rendered, when in fact there are likely several - this doesn't change any of the points I'm making):

  1. the useEffect in the child fires, because it always does after the first render. This calls the updatePrice function in the parent component. (PS: you probably need to call it as updatePrice(rating, price) instead of updatePrice({rating,price}) as you currently are.)

  2. this function calls setPrices, which updates its own prices state.

  3. because prices is in the dependency array of the useCallback that defines updatePrice, that function changes identity (becomes a new function reference)

  4. because its updatePrice prop has changed identity, the child component rerenders

  5. and because it's updatePrice in particular that has changed, which is (correctly!) in the dependency array for the useEffect highlighted at 3 above, that effect runs again and updatePrice is called

And we now repeat from 1), resulting in an infinite rerender cycle.

Although I'm not entirely sure what behaviour you need to implement here - which I would need to know if there was no simple solution to this problem - in this case there is a straightforward way of rewriting your code that breaks the cycle. It is the definition of updatePrice in the parent component, and how it sets the state (definition repeated below for ease of reference):

const updatePrice = useCallback((selectedRating, price) => {
  const new_prices = { ...prices, [selectedRating]: price }
  setPrices(new_prices)
}, [prices]);

While it's correct to have prices in the dependency array here, since the function definition as currently written depends on it - it doesn't "really" depend on it. It only needs to know the current prices state to set a new value that's calculated from it (as well as from the function arguments). There is an alternative way to write a state updating function in this case - with a function argument that describes how to get from the old to the new state. In your case that would give you:

const updatePrice = useCallback((selectedRating, price) => {
  setPrices(old_prices => ({ ...old_prices, [selectedRating]: price }));
}, []);

As well as rewriting the function body, I've also removed prices from the dependency array - because that's the crucial point here. Your function no longer mentions prices at all, so there's no need whatsoever to have it as a dependency. And without that dependency, you'll be free of your infinite render cycle, because now calling updatePrices from the child component doesn't cause a rerender of the child. It updates the prices in the parent, as presumably intended, but this no longer alters the identity of the updatePrice function itself, so there will be no rerender of the child.

If you've got a long time for a "deep dive" into React and the new ways of thinking with Hooks, I highly recommend this blog post from Dan Abramov, which despite the title referring just to useEffect, covers a lot of these things, including the use as I've done here of the "functional update" from of setState to avoid unnecessary dependencies, and goes further into more complex cases where the solution is to use useReducer rather than useState. Highly recommended, but not strictly necessary to answer your question - I hope my answer helps by itself [:)]

  • Related