Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to run useEffect once even if there're dependencies? And why ESLint is complaining about it?

Consider the following example:

const userRole = sessionStorage.getItem('role');
const { data, setData, type, setTableType } = useTable([]);

useEffect(() => {
  const getData = async () => {
    // fetch some data from API
    const fetchedData = await axios('..');
    
    if (userRole === 'admin') {
     setData([...fetchedData, { orders: [] }]);
    } else {
     setData(fetchedData);
    }
    
    if (type === '1') {
     setTableType('normal');
    }
  };
  getData();
}, []);

I want to run this effect ONLY on mounting and that's it, I don't care whether the userRole or setData has changed or not!

So, Now my questions are:

  1. Why ESLint is complaining about userRole being missing in the dependencies array? Its not even a state!
  2. Why ESLint is complaining about setData being missing in the dependencies array isn't it will always be the same reference or does it change?
  3. How to achieve what I want without upsetting the linter? Or should I straight slap the good old // eslint-disable-line react-hooks/exhaustive-deps line? Or this is absolutely barbaric?
  4. Why this indicates a bug in my code? I want to run this effect only once with whatever is available initially.

EDIT: Let me rephrase the question What If these variables change and I don't care about their new values? Especially when type is getting updated in different places in the same file. I just want to read the initial value and run useEffect once how to achieve that?

react-hooks/exhaustive-deps warning

like image 829
Mohamed Wagih Avatar asked Jul 16 '20 20:07

Mohamed Wagih


People also ask

How do I run useEffect only once with dependencies?

Side Effect Runs Only Once After Initial Render You can pass an empty array as the second argument to the useEffect hook to tackle this use case. useEffect(() => { // Side Effect }, []); In this case, the side effect runs only once after the initial render of the component.

How do you fix missing dependency warning when using useEffect React?

The warning "React Hook useEffect has a missing dependency" occurs when the useEffect hook makes use of a variable or function that we haven't included in its dependencies array. To solve the error, disable the rule for a line or move the variable inside the useEffect hook.

What happens if you don't specify the dependencies list in useEffect?

What happens if you don't specify the dependencies list in useEffect? If you don't specify it, the effect runs after each render. If it's empty ( [] ), the effect runs once, after the initial render. It must — or as we'll see later, should — contain the list of values used in the effect.

Can a useEffect have multiple dependencies?

Multiple dependencies log(“This useEffect will run either data or siteUrl changes”, data, siteUrl); }, [data, siteUrl]); In the above scenario, useEffect will run when either value of 'data' or 'siteUrl' changes. We can also give more dependencies according to our requirements.


4 Answers

Edit: After an update to React, it seems Eslint has begun complaining about the below solution, so I'd probably use // eslint-disable-line

Note: This answer is not written with React 18's double useEffect in mind.


You're correct in that giving useEffect an empty dependencies array is the way to go if you want the effect to run only once when the component mounts. The issue that ESLint is warning you about is the potential for the effect to execute using stale data, since it references external state properties, but as you've noticed, giving the array the dependencies it asks for causes the effect to run whenever any of them changes as well.

Luckily there is a simple solution that I'm surprised hasn't been yet mentioned — wrap the effect in a useCallback. You can give the dependencies to useCallback safely without it executing again.

// Some state value
const [state, setState] = useState();

const init = useCallback(
  () => {
    // Do something which references the state
    if (state === null) {}
  }, 
  // Pass the dependency to the array as normal
  [state]
);

// Now do the effect without any dependencies
useEffect(init, []);

Now init will re-memoize when a dependency changes, but unless you call it in other places, it will only actually be called for execution in the useEffect below.

To answer your specific questions:

  1. ESLint complains about userRole because React components re-run for every render, meaning userRole could have a different value in the future. You can avoid this by moving userRole outside your function.
  2. Same as previous case, although setData may realistically always the same, the potential for it to change exists, which is why ESLint wants you to include it as a dependency. Since it is part of a custom hook, this one cannot be moved outside your function, and you should probably include it in the dependencies array.
  3. See my primary answer
  4. As I may have already explained in the first 2, ESLint will suspect this to be a bug due to the fact that these values have the potential to change, even if they don't actually do so. It may just be that ESLint doesn't have a special case for checking "on mount" effects, and thus if it checks that effect like any other effect which may trigger several times, this would clearly become a realistic bug. In general, I don't think you need to worry too much about dependency warnings if your effect runs only once and you know the data is correct at that time.
like image 83
Magnus Bull Avatar answered Oct 19 '22 08:10

Magnus Bull


Linting is a process of analyzing code for potential errors. Now when we talk about why do we get a lint error, we need to understand that the rules were set by keeping in mind the ideal use cases of particular functionality.

Here incase of a useEffect hook, the general notion says that if we have a value which might change or which might lead to a change in logic flow, all those should go into the dependencies array.

So, data is the first candidate to go in. Similar is the case with userRole as it is being used to control the logic flow and not simply as a value.

Going with linter suggestion to ignore the error is what I recommend.

like image 31
Rohan Agarwal Avatar answered Oct 19 '22 08:10

Rohan Agarwal


  1. User role is in the useEffect thus it's a dependency (if it will change - the useEffect is invalid)
  2. the useEffect doesn't know if it will be the same or not, that's why it's asking for a dependency
  3. Usually do what the linter is asking, and add those two in the dependency array.
const userRole = sessionStorage.getItem('role');
const { data, setData } = useTable([]);

useEffect(() => {
  const getData = async () => {
    // fetch some data from API
    const fetchedData = await axios('..');
    
    if (userRole === 'admin') {
     setData([...fetchedData, { orders: [] }]);
    } else {
     setData(fetchedData);
    }
  };
  getData();
}, [userRole, setData]);
  1. here's Dan Abramov's take on this

“But I only want to run it on mount!”, you’ll say. For now, remember: if you specify deps, all values from inside your component that are used by the effect must be there. Including props, state, functions — anything in your component.

like image 3
k.s. Avatar answered Oct 19 '22 09:10

k.s.


Another solution is to create a hook for initializing (running once).

import { useState } from 'react';

export const useInit = initCallback => {
  const [initialized, setInitialized] = useState(false);

  if (!initialized) {
    initCallback();
    setInitialized(true);
  }
};

Then use it on you React components:

useInit(() => {
  // Your code here will be run only once
});
like image 2
Gustavo Dias Avatar answered Oct 19 '22 07:10

Gustavo Dias