Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding the React Hooks 'exhaustive-deps' lint rule

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)

like image 777
Logan Wlv Avatar asked Nov 14 '19 21:11

Logan Wlv


People also ask

What is the purpose of ESLint plugin for Hooks?

This ESLint plugin enforces the Rules of Hooks. It is a part of the Hooks API for React.

Can you have multiple useEffects?

You can have multiple useEffects in your code and this is completely fine! As hooks docs say, you should separate concerns. Multiple hooks rule also applies to useState - you can have multiple useState in one component to separate different part of the state, you don't have to build one complicated state object.

Do useEffect Hooks run in order?

Documentation mentions that by using this hook, we tell react that your component needs to do something after the it is done rendering. IMPORTANT: After the component is done rendering, it checks each useEffect hook sequentially in order which they are written.


2 Answers

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.

like image 189
Retsam Avatar answered Oct 11 '22 09:10

Retsam


The main purpose of the exhaustive-deps warning is to prevent the developers from missing dependencies inside their effect and lost some behavior.

Dan abramov – developer on Facebook core – strongly recommend to keep that rule enabled.

For the case of passing functions as dependencies, there is a dedicated chapter in the React FAQ:

https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies

tl;dr

If you have to put a function inside your dependencies array:

  • Put the function outside of your component, so you are sure that the reference won't be changed on each render.
  • If you can, call the function outside of your effect, and just use the result as dependency.
  • If the function must be declared in your component scope, you have to memoize the function reference by using the useCallback hook. The reference will be changed only if the dependencies of the callback function change.
like image 26
Freez Avatar answered Oct 11 '22 10:10

Freez