Reputation: 2756
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.
Upvotes: 6
Views: 3334
Reputation: 505
You 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 />
component.
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 component re-renders, a new instance of handleChange
method is created which is not equal to the previous handlechange method.
The component rendering flow will be like this:
<App />
component creates handleChange
method and passes it to the <Comp />
<Comp />
will be called after initial rendering and from there <App />
component's handleChange method will be called.<App />
component and re-renders it. While re-rendering new instance of handleChange is created and passed on onChange prop to <Comp />
component.To prevent this, handleChange method should be wrapped with useCallback. The callback function passed to useCallback hook will be memoized so when the child component compares the old and new prop they remain equal.
{} === {} // false
[] === [] // false
(() => {}) == (() => {}) //false
Upvotes: 8