Mohamed Wagih
Mohamed Wagih

Reputation: 1456

How to run useEffect once even if there are 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

Upvotes: 25

Views: 12780

Answers (7)

This is because you are using reactive values, which include props and all variables and functions declared directly inside of your component. To remove a dependency, you will need to prove that it’s not a dependency. In other words, all props, variables and functions need to be hardcoded as a const that will never change.

This will make your code more understandable since it is no longer using dependency variables that may or may not change.

Refer to https://react.dev/learn/removing-effect-dependencies#removing-unnecessary-dependencies

Upvotes: 0

ChandrasekarG
ChandrasekarG

Reputation: 1383

You could probably try to create a custom hook that can solve this problem.

  const useMountEffect = () =>
    useEffect(() => {
      // Your code that has dependencies
    }, []);

  useMountEffect();

This way, the code runs exactly once even if the dependencies change and the component re-renders. This solves the issue with the es-lint as well.

Upvotes: 1

Magnus Bull
Magnus Bull

Reputation: 1146

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.

Upvotes: 4

Gustavo Dias
Gustavo Dias

Reputation: 359

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
});

Upvotes: 7

Ross Allen
Ross Allen

Reputation: 44880

Caveat: I do not suggest actually using this. This is implementing incorrect logic and is guaranteed to be wrong when a value changes. The linter is trying to help because the code below introduces many subtle bugs.

If you want to do this though, you could use a ref to store the one-time function:

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

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

useEffect(() => {
  _dangerousOnMount.current();
}, []);

Upvotes: 1

Rohan Agarwal
Rohan Agarwal

Reputation: 2609

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.

Upvotes: 1

k.s.
k.s.

Reputation: 3004

  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.

Upvotes: 3

Related Questions