React hooks: not magic, just arrays
I am a huge fan of the new hooks API. However, it has some odd constraints about how you need to use it. Here I present a model for how to think about using the new API for those that are struggling to understand the reasons for those rules.
Unpacking how hooks work
I have heard some people struggling with the ‘magic’ around the new hooks API draft proposal so I thought I would attempt to unpack how the syntax proposal works at least on a superficial level.
The rules of hooks
There are two main usage rules the React core team stipulates you need to follow to use hooks which they outline in the hooks proposal documentation.
- Don’t call Hooks inside loops, conditions, or nested functions
- Only Call Hooks from React Functions
The latter I think is self evident. To attach behaviour to a functional component you need to be able to associate that behaviour with the component somehow.
The former however I think can be confusing as it might seem unnatural to program using an API like this and that is what I want to explore today.
State management in hooks is all about arrays
To get a clearer mental model, let’s have a look at what a simple implementation of the hooks API might look like.
Please note this is speculation and only one possible way of implementing the API to show how you might want to think about it. This is not necessarily how the API works internally.
How could we implement `useState()` ?
Let’s unpack an example here to demonstrate how an implementation of the state hook might work.
First let’s start with a component:
The idea behind the hooks API is that you can use a setter function returned as the second array item from the hook function and that setter will control state which is managed by the hook.
So what’s React going to do with this?
Let’s annotate how this might work internally within React. The following would work within the execution context for rendering a particular component. That means that the data stored here lives one level outside of the component being rendered. This state is not shared with other components but it is maintained in a scope that is accessible to subsequent rendering of the specific component.
Create two empty arrays: `setters` and `state`
Set a cursor to 0
2) First render
Run the component function for the first time.
useState() call, when first run, pushes a setter function (bound to a cursor position) onto the
setters array and then pushes some state on to the
3) Subsequent render
Each subsequent render the cursor is reset and those values are just read from each array.
4) Event handling
Each setter has a reference to its cursor position so by triggering the call to any
setter it will change the state value at that position in the state array.
And the naive implementation
Following here is a naive code example to demonstrate that implementation.
Note: This is not representative of how hooks work at all under the hood but it should give you an idea of a good way to think about how hooks work for a single component. That is why we are using module level vars etc.
Why order is important
Now what happens if we change the order of the hooks for a render cycle based on some external factor or even component state?
Let’s do the thing the React team say you should not do:
Here we have a
useState call in a conditional. Let’s see the havoc this creates on the system.
Bad Component First Render
At this point our instance vars
lastName contain the correct data but let’s have a look what happens on the second render:
Bad Component Second Render
lastName are set to
“Rudi” as our state storage becomes inconsistent. This is clearly erroneous and doesn’t work but it gives us an idea of why the rules for hooks are laid out the way they are.
The React team are stipulating the usage rules because not following them will lead to inconsistent data
Think about hooks manipulating a set of arrays and you wont break the rules
So now it should be clear as to why you cannot call `use` hooks within conditionals or loops. Because we are dealing with a cursor pointing to a set of arrays, if you change the order of the calls within render, the cursor will not match up to the data and your use calls will not point to the correct data or handlers.
So the trick is to think about hooks managing its business as a set of arrays that need a consistent cursor. If you do this everything should work.
Hopefully I have laid out a clearer mental model for how to think about what is going on under the hood with the new hooks API. Remember the true value here is being able to group concerns together so being careful about order and using the hooks API will have a high payoff.
Hooks is an effective plugin API for React Components. There is a reason why people are excited about this and if you think about this kind of model where state exists as a set of arrays then you should not find yourselves breaking the rules around their usage.