Reputation: 1456
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:
userRole
being missing in the
dependencies array? Its not even a state!setData
being missing in the dependencies array
isn't it will always be the same reference or does it change?// eslint-disable-line react-hooks/exhaustive-deps
line? Or this is absolutely barbaric?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
Reputation: 71
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
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
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:
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.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.Upvotes: 4
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
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
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
Reputation: 3004
useEffect
thus it's a dependency (if it will change - the useEffect
is invalid)useEffect
doesn't know if it will be the same or not, that's why it's asking for a dependencyconst 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]);
“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