I'm having a hard time understanding the 'exhaustive-deps' lint rule.
I already read this post and this post but I could not find an answer.
Here is a simple React component with the lint issue:
const MyCustomComponent = ({onChange}) => {
const [value, setValue] = useState('');
useEffect(() => {
onChange(value);
}, [value]);
return (
<input
value={value}
type='text'
onChange={(event) => setValue(event.target.value)}>
</input>
)
}
It requires me to add onChange
to the useEffect
dependencies array. But in my understanding onChange
will never change, so it should not be there.
Usually I manage it like this:
const MyCustomComponent = ({onChange}) => {
const [value, setValue] = useState('');
const handleChange = (event) => {
setValue(event.target.value);
onChange(event.target.value)
}
return (
<input value={value} type='text' onChange={handleChange}></input>
)
}
Why the lint? Any clear explanation about the lint rule for the first example?
Or should I not be using useEffect
here? (I'm a noob with hooks)
The reason the linter rule wants onChange
to go into the useEffect
hook is because it's possible for onChange
to change between renders, and the lint rule is intended to prevent that sort of "stale data" reference.
For example:
const MyParentComponent = () => {
const onChange = (value) => { console.log(value); }
return <MyCustomComponent onChange={onChange} />
}
Every single render of MyParentComponent
will pass a different onChange
function to MyCustomComponent
.
In your specific case, you probably don't care: you only want to call onChange
when the value changes, not when the onChange
function changes. However, that's not clear from how you're using useEffect
.
The root here is that your useEffect
is somewhat unidiomatic.
useEffect
is best used for side-effects, but here you're using it as a sort of "subscription" concept, like: "do X when Y changes". That does sort of work functionally, due to the mechanics of the deps
array, (though in this case you're also calling onChange
on initial render, which is probably unwanted), but it's not the intended purpose.
Calling onChange
really isn't a side-effect here, it's just an effect of triggering the onChange
event for <input>
. So I do think your second version that calls both onChange
and setValue
together is more idiomatic.
If there were other ways of setting the value (e.g. a clear button), constantly having to remember to call onChange
might be tedious, so I might write this as:
const MyCustomComponent = ({onChange}) => {
const [value, _setValue] = useState('');
// Always call onChange when we set the new value
const setValue = (newVal) => {
onChange(newVal);
_setValue(newVal);
}
return (
<input value={value} type='text' onChange={e => setValue(e.target.value)}></input>
<button onClick={() => setValue("")}>Clear</button>
)
}
But at this point this is hair-splitting.