Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 686
UjinT34 Avatar asked Apr 03 '19 10:04

UjinT34


People also ask

Can we call React Hooks conditionally?

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.

Why Hooks Cannot be called conditionally?

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.

What is the practice to avoid when using React Hooks?

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.


2 Answers

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.

like image 152
Estus Flask Avatar answered Sep 20 '22 14:09

Estus Flask


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

like image 31
Luciano Semerini Avatar answered Sep 17 '22 14:09

Luciano Semerini