How to run useEffect once even if there are dependencies? And why ESLint is complaining about it?
Asked Answered
O

7

24

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

Outlawry answered 16/7, 2020 at 20:41 Comment(11)
If userRole changes, won't the data be incorrect then?Pescara
@RossAllen userRole is stored in sessionStorage and it will always be the same unless the user logged out and logged in with different account typeOutlawry
Yes, #3, yes. Always been doing thisBenzoate
"unless the user logged out and logged in with different account type" <- this is why userRole should be in the dependencies array. The code doesn't know the business logic of "it will always be the same".Pescara
Also, the eslint rule is aware that the second value returned from React.useState does not change over time, but useTable appears to be a custom library; it doesn't know anything special about setData and so gives you the same warning as any other variable.Pescara
@RossAllen Same goes for any variable. I stated clearly that I don't care about whether the variables change or not I want to run useEffect once like componentDidMount without upsetting the linter.Outlawry
@RossAllen the useTable is a custom hook I wrote which has state and setState and other functions that I expose.Outlawry
If these things don't change over time then the useEffect will only get called once. If these things ever do change, your code will be logically incorrect and will lead to subtle errors. I suggest following the lint rule; it is helping you here and preventing subtle potential bugs.Pescara
@RossAllen Ok let me rephrase the question what If they do change and I don't care? I just want to read the initial value and run useEffect once how to achieve that?Outlawry
You already answered that in 3., right? "// eslint-disable-line react-hooks/exhaustive-deps". These rules are correctly identifying that the logic in your code is incorrect though. Ignoring this warning means you might ignore it in the future and your code will be incorrect when those values change. The fact that you know they won't change is fine; then the useEffect will only ever run one time like you want. If you add the vals to the array, your code will then also be logically correct.Pescara
@RossAllen Exactly! I intentionally want to run this piece of code only once on mounting even if any of these variables change, I updated the question with another variable type to help delivering my point. This type changes whenever the user clicks on a button to go to the next table. In this case If I added the type to dependency array it will get triggered which what I don't want! Can you check the post one last time and tell me what do you think?Outlawry
R
6

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
});
Rhpositive answered 1/11, 2021 at 17:14 Comment(2)
This seems the cleanest approach to meJosefajosefina
Is it possible to still do effect cleanup?Coxcomb
F
4

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.
Freezer answered 20/1, 2021 at 8:1 Comment(2)
You still have to pass init to the useEffect dep array in order to satisfy react-hooks/exhaustive-deps, so this doesn't solve the problemDismay
@Dismay Yes, I added a note at the top of the answer on Jun 9, 2022. Before that, react-hooks/exhaustive-deps didn't warn about it, which in hindsight was probably a bug rather than a feature.Freezer
M
3
  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.

Matrilineal answered 16/7, 2020 at 20:52 Comment(7)
I appreciate your answer but let me rephrase my question: What If they do change and I don't care? I just want to read the initial value and run useEffect once how to achieve that?Outlawry
is userRole used anywhere else in the app ?Matrilineal
@Mohamed move const userRole = sessionStorage.getItem('role'); outside the component body, or do const [userRole] = useState(sessionStorage.getItem('role')); The former will allow you to omit userRole from the dependencies list, while the latter still requires you to include it in the dependencies list, but the useEffect() won't rerun if the sessionStorage updates in between renders of the component.Tamboura
@Mohamed sorry, misspoke, do you use it anywhere in this component? if not, you can just put it in the useEffect hook, and that's it.Matrilineal
@PatrickRoberts Actually this will solve the userRole issue correct let me add another variable to deliver my point.Outlawry
@Matrilineal Yes you're correct as well I just wanted to deliver a point let me edit the postOutlawry
@Mohamed you need to add both setTableType and type to the useEffect dependenciesMatrilineal
O
1

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.

Oxford answered 16/7, 2020 at 20:51 Comment(5)
I appreciate your answer! But going with what the linter wants will trigger useEffect to run whenever any of the variables inside the dependency array change! Which what I don't want!Outlawry
Correct me if am wrong but you want to know of a way to solve all flags raised by linter not by commenting it out but my using the dependency array BUT want it to run only once?Oxford
Yes! I want to leave the dependency array empty to make the useEffect run only once without making the linter angryOutlawry
Since you are using a custom hook am not sure on how to make the linter happy but run it only once. However, there exists a hackish way (useRef) to keep your array non-empty and run it once and hence keeping the linter happy. Though I would suggest against it.Oxford
I'm looking for the correct way to do it since I'm very confused and the documentation doesn't mention this case!Outlawry
P
1

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();
}, []);
Pescara answered 16/7, 2020 at 22:4 Comment(1)
Hmm this seems kinda hacky to me I was looking for a more clear way to do it but you gave me a very good advice "If these things don't change over time then the useEffect will only get called once. If these things ever do change, your code will be logically incorrect and will lead to subtle errors. I suggest following the lint rule; it is helping you here and preventing subtle potential bugs" I appreciate it really.Outlawry
E
1

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.

Edison answered 15/6, 2022 at 7:6 Comment(0)
P
0

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

Priggish answered 6/3 at 5:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.