Say I have a parent component, which renders a child component, 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.
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 component, and control the child with the parent, but that is not really what I want. In real life, my child component takes care of more than a simple counter, and I want to use it easily from the parent. There is complex behaviour inside of the child component and I want to communicate 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 component will update from several separate places in the component 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 recommended, React community refers to this explanation. Which I understand, only I feel that this situation would be an exception?** Also, I use CRA, which comes with great linters etc out of the box, and the linter complains 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, community-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 component, 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.