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):
the
useEffect
in the child fires, because it always does after the first render. This calls theupdatePrice
function in the parent component. (PS: you probably need to call it asupdatePrice(rating, price)
instead ofupdatePrice({rating,price})
as you currently are.)this function calls
setPrices
, which updates its ownprices
state.because
prices
is in the dependency array of theuseCallback
that definesupdatePrice
, that function changes identity (becomes a new function reference)because its
updatePrice
prop has changed identity, the child component rerendersand because it's
updatePrice
in particular that has changed, which is (correctly!) in the dependency array for theuseEffect
highlighted at 3 above, that effect runs again andupdatePrice
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 [:)]