react: Rules of Hooks don't support top-level exit conditions
Do you want to request a feature or report a bug? bug
What is the current behavior?
The following piece of code breaks the rules of hooks:
import {useState} from 'react';
const MyComponent = () => {
const [foo, setFoo] = useState ();
if ( !foo ) return;
const [bar, setBar] = useState ();
return null;
};
The lint rule reports:
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
What is the expected behavior?
My understanding is that for hooks to work they need to be called always in the same order, otherwise returned values can’t be properly mapped to function calls.
In my piece of code the hooks are always called in the same order, but some times we exit early, which is slightly beneficial both for performance and for writing clean code.
Shouldn’t this just work?
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React? v16.8.6
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 10
- Comments: 17 (2 by maintainers)
We could in theory support them. However we’ve chosen intentionally against that because semantics get very confusing.
What would happen to state below? Would it be reset or preserved? What about effects? Would they be cleaned up (like on unmount) or would they run normally?
If you think about it you’ll find cases that are very confusing regardless of the behavior you pick. Therefore, we disallow this pattern altogether. Put early return after calls to Hooks.
@audiolion isn’t that breaking the rules of hooks too? You’re still conditionally calling them inside your custom hook.
Honestly I think the simplest work-around to this is to create a higher order component that decides if it should render its child or null. Still, I wish the lint rule would allow you to avoid this sort of work-around.
@Dergash no that will still crash you in development mode when you go from the non-early exit version to the early-exit version of the component, regardless of how many hooks you have in it. I must admit I’ve seen code like that in my project too and it didn’t crash, but it could crash if the component ever went back to the early exit version I think.
TBH I don’t see any way around this other than the wrapper component strategy. It’s a necessity because that’s how react organizes hooks: by component. I’m not terribly concerned about this adding overhead. Should I be? This is JavaScript. It’s fine. Usually we should look at memoizing things to prevent needless re-renders first anyway, right?
Here’s an example of the solution I’m proposing for working around this.
Original component that breaks the rule:
New code that works around the lint rule:
I often have to create tiny wrapper components like this for other reasons, anyway, such as exploiting the
keyproperty to cause re-renders.I believe the “top level” phrasing is a two-sided sword: from one side, it is a simple concept to remember and explain to other developers, but from the other side, it can get in a way of understanding the reasons behind this rule (which are of course explained below the title).
According to those reasons, it is important to preserve the order and number of hook calls between the render. This is achieved by rules of prohibiting things that may change the order and the number of hooks.
In the solution that @audiolion proposed he avoids changing both, because all of the hook calls are placed after return statement, so they will never change. In fact I think this is what had to be done if you need to optimise things in case your hooks are computationally expensive and you don’t want them to fire if you will hit early return. So it can placed before or after, but if you mix it (before and after) then you break the rule@togakangaroo you might have mentioned the wrong person 😂