We recently started using React Hooks in one of the apps here at Flatiron School. I’ve used React before, but haven’t had the chance to explore Hooks until now so I was pretty excited! I dove into the documentation to get a feel for it and saw this section about the Rules of Hooks, which state that you should:
Only Call Hooks at the Top Level
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. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect callsOnly Call Hooks from React Functions
The first rule was particularly interesting to me. It seemed easy enough to follow, but why did the rule exist? What exactly happens if Hooks aren’t called in the exact same order every single time a component is rendered? How convenient that the doc writers linked to an explainer! To summarize the explanation, if Hooks aren’t called in the same order each time a component is rendered, you could have cascading failures where you miss one Hook and none of the subsequent ones are recognized properly. The Hook calls could end up shifted and you’ll get unexpected bugs.
Well, that seems pretty bad. Let’s definitely not allow that to happen. So we now know why the rule shouldn’t be broken, but not why things break. I still really wanted to understand why call order was so important to Hooks. To figure that out I decided to jump into the source code. I started here where the Hooks are defined and eventually followed it over to here where the Dispatcher is. That’s where I saw this note:
Hooks are stored as a linked list on the fiber's memoizedState field. The current hook list is the list that belongs to the current fiber. The work-in-progress hook list is a new list that will be added to the work-in-progress fiber.
I won’t be delving into the exact setup of React Hooks as linked lists, but for a detailed breakdown you should check out Linked lists in the wild: React Hooks, which is well worth the read. For our purposes here, we just need to understand that on the first render of a component, a linked list of the Hooks called gets created and on subsequent renders, that list has to be traversable.
This is why we need to make sure Hooks are called in the same order on every render. If one of the Hooks is wrapped in a conditional instead of being called at the top level you could end up with a different number of Hooks on re-render. That means you would end up with a node missing on your linked list of Hooks. If a node is missing, the previous node won’t be referencing the correct thing and the subsequent nodes will be out of order. You could end up with something like this shown in the Rules of Hooks explainer:
useState('Mary') // 1. Read the name state variable (argument is ignored)
// useEffect(persistForm) // 🔴 This Hook was skipped!
useState('Poppins') // 🔴 2 (but was 3). Fail to read the surname state variable
useEffect(updateTitle) // 🔴 3 (but was 4). Fail to replace the effect
Unsurprisingly, this leads to failures. Also you’ll likely see errors like
Rendered fewer hooks than expected or
Rendered more hooks than during the previous render. While at first it struck me as odd that React Hooks are dependent on call order, looking at the code, it makes a lot of sense. With the linked list set up, disappearing or disordered nodes would cause cascading failures and nobody wants that.
Thanks for reading! Want to work on a mission-driven team that loves React and deep dives into source code? We’re hiring!