Is it safe to call react hooks based on a constant condition?

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".

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"/>

Is it safe to call react hooks based on a constant condition?, 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� Only call Hooks from React function components. Don’t call Hooks from regular JavaScript functions. Don’t call Hooks from regular JavaScript functions. Thus this is how React Hooks can be useful in making API calls, sometimes we have to convert a functional component into a class component only because of not being able to manage the state

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

Is it safe to call react hooks based on a constant condition? – React , Hooks between multiple useState and useEffect calls. And there is a place for exceptions to this rule: “Don't call Hooks inside loops, conditions,� Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class. This page describes the APIs for the built-in Hooks in React. If you’re new to Hooks, you might want to check out the overview first. You may also find useful information in the frequently asked questions section. Basic Hooks. useState

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.

Hooks at a Glance – React, import React, { useState } from 'react'; function Example() { // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); return� The existing React hooks 🍱 The new API comes with two main pre-existing hooks, and some others for other use cases. Basics React hooks. The foundation of all React hooks, every other hook you will see is a variation of these three or are using them as primitives. The useState is the State hook use it for declaring the state in your components

Please don't use this pattern. It may work in your example but it is not nice (or idiomatic).

The standard pattern (for good reason) is that initial state is declared in the constructor and then updated in response to some condition in the body (setState). React Hooks mirror this functionality in stateless components - so it should work the same.

Secondly, I cannot see how it is useful to dynamically add this piece of state and potentially cause rendering problems later down the line. In your example, a simple const would work just as well - there is no reason to use dynamic state.

Consider this:

return (<React.Fragment>{second}</React.Fragment>)

This breaks with a Reference error whenever you don't have second defined.

Best Practices With React Hooks — Smashing Magazine, Call Hooks At The Top Level. Don't call Hooks inside loops, conditions, or nested functions. 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. We declare a state variable called count, and set it to 0.React will remember its current value between re-renders, and provide the most recent one to our function. If we want to update the current count, we can call setCount.

Frustrations with React Hooks, export const Heading: React.FC<HeadingProps> There are several well- documented problems with the class-based lifecycle events. One of the Don't call Hooks inside loops, conditions, or nested functions. Instead� Therefore, if the condition is true, the element right after && will appear in the output. If it is false, React will ignore and skip it. Inline If-Else with Conditional Operator . Another method for conditionally rendering elements inline is to use the JavaScript conditional operator condition ? true : false.

How the useEffect Hook Works (with Examples), import React, { useEffect, useState } from 'react'; import ReactDOM from return a function from here, and React will call // it prior to unmounting. return of // whether the LifecycleDemo is shown or hidden const [mounted, time the component renders, which is why it's safe for an effect to depend on one. useState and useEffect are some of the React built-in Hooks.useState works in a way that it returns two values: one is the state value, and the other one is its setter. By array destructuring, you can set the name of those two values to anything you like.

How to break the rules of React Hooks, Don't call Hooks inside loops, conditions, or nested functions. If we change the calling order of Hooks within a component, React gets confused and breaks, const List = ({ items }) => { // Loop over items with .map return� Only call hooks from React functions or custom hooks. Since React components are re-rendered each time data changes, this means the exact same hooks must be called in the exact same order on every single render. If we wrapped our hooks in a conditional or function, the state would sometimes be created and other times wouldn’t.

Comments
  • Thanks. I understand that TestState relies on the current 'per element' requirement and might break if some internal react changes turn it into 'per component' requirement. But I can't imagine how const DEBUG might break in the future. Also const [isDebug] = useState(debug); is written this way to ensure that it doesn't change.
  • @UjinT34, at present you are not modifying the isDebug state, but someone in future may just add a setter function and provide a method to modify isDebug state. This may break the app.