Is it safe to call react hooks based on a constant condition?
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.
Although you can write hooks conditionally like you mentioned above and it may work currently, it can lead to unexpected behavior in the future. For instance in the current case you aren't modifying the isDebug
state.
Demo
const {useState, useEffect} = React;
function TestState({id, debug}) {
const [isDebug, setDebug] = useState(debug);
if (isDebug) {
useEffect(() => console.log('rendered', id));
}
const toggleButton = () => {
setDebug(prev => !prev);
}
return (
<div>
<span>{id}</span>
<button type="button" onClick={toggleButton}>Toggle debug</button>
</div>
);
}
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => setCounter(1), []);
return (
<div>
<TestState id="1" debug={false}/>
<TestState id="2" debug={true}/>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="app"/>
As a rule of thumb you shouldn't violate the rules since it may cause problems in future. You could handle the above scenarios in the following way without violating the rules
const {useState, useEffect} = React;
function TestState({id, debug}) {
const [isDebug, setDebug] = useState(debug);
useEffect(() => {
if(isDebug) {
console.log('rendered', id)
}
}, [isDebug]);
const toggleButton = () => {
setDebug(prev => !prev);
}
return (
<div>
<span>{id}</span>
<button type="button" onClick={toggleButton}>Toggle debug</button>
</div>
);
}
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => setCounter(1), []);
return (
<div>
<TestState id="1" debug={false}/>
<TestState id="2" debug={true}/>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="app"/>
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