The Rules of Hooks require that the same hooks and in the same order are called on every render. And there is an explanation about what goes wrong if you break this rule. For example this code:
function App() {
console.log('render');
const [flag, setFlag] = useState(true);
const [first] = useState('first');
console.log('first is', first);
if (flag) {
const [second] = useState('second');
console.log('second is', second);
}
const [third] = useState('third');
console.log('third is', third);
useEffect(() => setFlag(false), []);
return null;
}
Outputs to console
render
first is first
second is second
third is third
render
first is first
third is second
And causes a warning or an error.
But what about conditions that do not change during the element lifecycle?
const DEBUG = true;
function TestConst() {
if (DEBUG) {
useEffect(() => console.log('rendered'));
}
return <span>test</span>;
}
This code doesn't really break the rules and seems to work fine. But it still triggers the eslint warning.
Moreover it seems to be possible to write similar code based on props:
function TestState({id, debug}) {
const [isDebug] = useState(debug);
if (isDebug) {
useEffect(() => console.log('rendered', id));
}
return <span>{id}</span>;
}
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => setCounter(1), []);
return (
<div>
<TestState id="1" debug={false}/>
<TestState id="2" debug={true}/>
</div>
);
}
This code works as intended.
So is it safe to call hooks inside a condition when I am sure that it is not going to change? Is it possible to modify the eslint rule to recognise such situations?
The question is more about the real requirement and not the way to implement similar behaviour. As far as I understand it is important to
ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls
And there is a place for exceptions to this rule: "Don’t call Hooks inside loops, conditions, or nested functions".
React basically knows which useEffect hook is which, basically by counting invocations. Calling useEffect conditionally is bad, specifically because the amount of times useEffect gets called cannot change. Your example is conditional, but React can't detect it because in either condition you call it once.
What is this? The code snippet above causes the error because we are calling the second useState hook conditionally. This is not allowed because the number of hooks and the order of hook calls have to be the same on re-renders of our function components.
Hooks should not be called within loops, conditions, or nested functions since conditionally executed Hooks can cause unexpected bugs. Avoiding such situations ensures that Hooks are called in the correct order each time the component renders.
This hook rule address common cases when problems that may occur with conditional hook calls:
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders.
If a developer isn't fully aware of consequences, this rule is a safe choice and can be used as a rule of thumb.
But the actual rule here is:
ensure that Hooks are called in the same order each time a component renders
It's perfectly fine to use loops, conditions and nested functions, as long as it's guaranteed that hooks are called in the same quantity and order within the same component instance.
Even process.env.NODE_ENV === 'development'
condition can change during component lifespan if process.env.NODE_ENV
property is reassigned at runtime.
If a condition is constant, it can be defined outside a component to guarantee that:
const isDebug = process.env.NODE_ENV === 'development';
function TestConst() {
if (isDebug) {
useEffect(...);
}
...
}
In case a condition derives from dynamic value (notably initial prop value), it can be memoized:
function TestConst({ debug }) {
const isDebug = useMemo(() => debug, []);
if (isDebug) {
useEffect(...);
}
...
}
Or, since useMemo
isn't guaranteed to preserve values in future React releases, useState
(as the question shows) or useRef
can be used; the latter has no extra overhead and a suitable semantics:
function TestConst({ debug }) {
const isDebug = useRef(debug).current;
if (isDebug) {
useEffect(...);
}
...
}
In case there's react-hooks/rules-of-hooks
ESLint rule, it can be disabled per line basis.
For your use-case I don't see the problem, I don't see how this can break in the future, and you are right that it works as intended.
However, I think the warning is actually legit and should be there at all times, because this can be a potential bug in your code (not in this particular one)
So what I'd do in your case, is to disable react-hooks/rules-of-hooks
rule for that line.
ref: https://reactjs.org/docs/hooks-rules.html
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With