Say I have a parent ponent, which renders a child ponent, and all the child does is keep some state and trigger an onChange handler if its own state changes. See the code below, or the CodeSandbox.
CodeSandbox example
This code gets into an infinite loop and I want to get rid of this behaviour.
Possible solutions, but ones I don't prefer:
1: I could put all state in the parent ponent, and control the child with the parent, but that is not really what I want. In real life, my child ponent takes care of more than a simple counter, and I want to use it easily from the parent. There is plex behaviour inside of the child ponent and I want to municate some simple changes to the parent. Or is this an absolute no-go in React? (to keep state in child and also trigger change on state update to parent) I would say it's not necessarily?
2: I could also trigger the onChange handler from within the handleInternalChange handler. Also, not what I want. In real life, my child ponent will update from several separate places in the ponent itself, so a state change is the most elegant thing to watch and trigger an onChange parent.
3: I could omit the onChange dependency in the useEffect dependency array. This is not remended, React munity refers to this explanation. Which I understand, only I feel that this situation would be an exception?** Also, I use CRA, which es with great linters etc out of the box, and the linter plains about it if I remove the onChange handler from the dependencies, and I don't prefer to start brewing my own liner rules. For such a simple use case as mine, munity-set linters should work fine.
What I think that happens What I think happens, is that the parent gets updated, then re-renders the whole thing, and somehow, the onChange handler is 'changed' too. The function doesn't actually change, as far as I would say, but React thinks (or knows) it does, so it triggers the useEffect call again in the child ponent, and then the endless loop is born.
But, as far as I'm concerned, the onChange function doesn't change. So why is the useEffect call being triggered? And how can I prevent this?
import React, { useState, useEffect } from "react";
const Comp = ({ onChange }) => {
const [internalState, setInternalState] = useState(0);
const handleChange = () => {
setInternalState(internalState + 1);
};
useEffect(() => {
const result = internalState.toString();
onChange(result);
}, [internalState, onChange]);
return (
<div onClick={handleChange}>
CLICK ME
<div>{`INTERNAL NUM: ${internalState}`}</div>
</div>
);
};
export default function App() {
const [state, setState] = useState("");
const handleChange = () => setState(state + 1);
return (
<div className="App">
<h3>{`STATE: ${state}`}</h3>
<Comp onChange={handleChange} />
</div>
);
}
** After some though, in other cases, the onChange prop could of course be changed, simply by assigning a different function to the prop. So this rule is perfectly clear to me. Only (as said in last paragraph) why is it behaving like it changes in this scenario? Since my function does not change at all.
Say I have a parent ponent, which renders a child ponent, and all the child does is keep some state and trigger an onChange handler if its own state changes. See the code below, or the CodeSandbox.
CodeSandbox example
This code gets into an infinite loop and I want to get rid of this behaviour.
Possible solutions, but ones I don't prefer:
1: I could put all state in the parent ponent, and control the child with the parent, but that is not really what I want. In real life, my child ponent takes care of more than a simple counter, and I want to use it easily from the parent. There is plex behaviour inside of the child ponent and I want to municate some simple changes to the parent. Or is this an absolute no-go in React? (to keep state in child and also trigger change on state update to parent) I would say it's not necessarily?
2: I could also trigger the onChange handler from within the handleInternalChange handler. Also, not what I want. In real life, my child ponent will update from several separate places in the ponent itself, so a state change is the most elegant thing to watch and trigger an onChange parent.
3: I could omit the onChange dependency in the useEffect dependency array. This is not remended, React munity refers to this explanation. Which I understand, only I feel that this situation would be an exception?** Also, I use CRA, which es with great linters etc out of the box, and the linter plains about it if I remove the onChange handler from the dependencies, and I don't prefer to start brewing my own liner rules. For such a simple use case as mine, munity-set linters should work fine.
What I think that happens What I think happens, is that the parent gets updated, then re-renders the whole thing, and somehow, the onChange handler is 'changed' too. The function doesn't actually change, as far as I would say, but React thinks (or knows) it does, so it triggers the useEffect call again in the child ponent, and then the endless loop is born.
But, as far as I'm concerned, the onChange function doesn't change. So why is the useEffect call being triggered? And how can I prevent this?
import React, { useState, useEffect } from "react";
const Comp = ({ onChange }) => {
const [internalState, setInternalState] = useState(0);
const handleChange = () => {
setInternalState(internalState + 1);
};
useEffect(() => {
const result = internalState.toString();
onChange(result);
}, [internalState, onChange]);
return (
<div onClick={handleChange}>
CLICK ME
<div>{`INTERNAL NUM: ${internalState}`}</div>
</div>
);
};
export default function App() {
const [state, setState] = useState("");
const handleChange = () => setState(state + 1);
return (
<div className="App">
<h3>{`STATE: ${state}`}</h3>
<Comp onChange={handleChange} />
</div>
);
}
** After some though, in other cases, the onChange prop could of course be changed, simply by assigning a different function to the prop. So this rule is perfectly clear to me. Only (as said in last paragraph) why is it behaving like it changes in this scenario? Since my function does not change at all.
Share Improve this question asked Dec 15, 2020 at 13:59 SventiesSventies 2,7864 gold badges36 silver badges55 bronze badges 4-
Your
useEffect
really doesn't make sense here,. Your getting an infinate loop, because youruseEffect
is getting called every time, and that effect is calling theonChange
callback, so infinite loop. – Keith Commented Dec 15, 2020 at 14:02 -
Since my function does not change at all.
It changes every time you render the parent. – Keith Commented Dec 15, 2020 at 14:08 -
Your
onChange
props keeps changing because every time the parent ponent changes it re-instantiateshandleChange
, which will never be the same of its previous self. I think you should go with solution 1: keeping the state in the parent and pass it down as a prop to the child ponent; this should not limit you in any way, as your child ponent can still have its own local state and do all the plex operations it has to do. – secan Commented Dec 15, 2020 at 14:08 - 2 You might want to check the useCallback hook – secan Commented Dec 15, 2020 at 14:11
1 Answer
Reset to default 8You should wrap the handleChange method with useCallback hooks, so that it will be created once.
const handleChange = useCallback(() => setState(state + 1),[]);
The infinite loop happens because you have added onChange
method as dependency for useEffect in the <Comp />
ponent.
useEffect takes array of dependencies and runs the effect if one of dependencies change.
Since you have added onchange handler as dependency, each time parent ponent re-renders, a new instance of handleChange
method is created which is not equal to the previous handlechange method.
The ponent rendering flow will be like this:
<App />
ponent createshandleChange
method and passes it to the<Comp />
- Useffect in the
<Comp />
will be called after initial rendering and from there<App />
ponent's handleChange method will be called. - state changes in
<App />
ponent and re-renders it. While re-rendering new instance of handleChange is created and passed on onChange prop to<Comp />
ponent. - Since the value at previous and new onChange prop is different, useEffect will fired which again will update parent ponent's state and this loop continues.
To prevent this, handleChange method should be wrapped with useCallback. The callback function passed to useCallback hook will be memoized so when the child ponent pares the old and new prop they remain equal.
{} === {} // false
[] === [] // false
(() => {}) == (() => {}) //false