Yet another OOP/C# person (me) trying to understand the mechanics behind React Hooks
Andrew Nosenko
Posted on September 30, 2020
I've written this article as a memo to my future self as well, and my goal was to make it short. If there is something here that isn't technically correct, a feedback would be appreciated.
What is the magic behind the simplicity of React Hooks?
Coming to React from an OOP/MVVM/C# background, for a while I was having this "how-does-it-work-behind-the-scence" syndrome about React hooks.
As they get called from what seemingly is a plain, stateless JavaScript function, and yet hooks maintain their state.
Particularly, about how multiple hooks of the same kind coexist within the same function component and persist their state across multiple renders.
For example, across multiple invocations of the following MyComponent
function (try it in the CodePen):
function MyComponent() {
const refUp = useRef(0);
const refDown = useRef(0);
const [countUp, setCountUp] = useState(0);
const [countDown, setCountDown] = useState(0);
const clicked = () => {
setCountUp(count => count + 1);
setCountDown(count => count - 1);
};
console.log("rendering");
return (
<p>
<span>Up: {refUp.current++}</span><br/>
<span>Down: {refDown.current--}</span><br/>
<span>Counts: {countUp}, {countDown}</span><br/>
<button onClick={clicked}>Count</button>
</p>
);
}
How is it possible that refA.current
and refB.current
can be mutated and still survive multiple renders, keeping their values, without relying upon something like JavaScript's this
?
Especially, given they both were created with two identical invocations of useRef(0)
? My guts were telling me there should be a unique name parameter, like useRef(0, "refA")
, but there isn't.
The same question applies to countUp
, countDown
and the corresponding useState(0)
calls which initialize these variables.
Something has got to maintain the state for us.
And there has to be some kind of 1:1 mapping for each hook into that state.
As it turns, there is no magic. In a nutshell, here is my understanding of how it goes:
First of all, hook calls don't work outside React function components, by design. They implicitly rely upon the calling context React provides them with, when it renders the component.
React maintains its own internal state for the life-time of the web page. While not exactly accurate, let's called it React's static state.
Each component like
MyComponent
above has a dedicated entry in React's static state, and that entry keeps the state of each hook used by the component between renders.When a hook like
useRef
is called, React knows which component is calling it (the one currently being rendered), so React can retrieve that individual component's state entry it have previously mapped and stored in its static state. That's where the current values of hooks likeuseRef
anduseState
are stored, per component.Initially, such entry gets created and mapped when the component gets mounted (or perhaps upon the first render, I didn't dig deep into that, but it's done once).
The exact order of calls like
useRef
oruseState
within the component function matters, and it should remain the same across subsequent renders. In our case, React initially creates two entries foruseRef
in its internal state forMyComponent
, then two entries foruseState
.Upon subsequent renders (invocations of
MyComponent
), React knows how to access the correct state and which values to return, by the order of eachuseRef
oruseState
call.I'm not sure about exact data structure used by React to map hooks by the order of their appearance in the function component, I didn't dig into that either. But it's easy to think about the order of each hook call as of an index in the array of hooks maintained by React for the life cycle of our component.
-
Thus, if we mess about this order across multiple renders, our state will be broken, because the original indexing wouldn't be correct. E.g., the following made-up example will likely screw up the state of
refUp
andrefDown
very soon, because their order ofuseRef
calls is inconsistent:
// don't mess up the order of hooks like this: let refUp; let refDown; if (Date.now() & 1) { refUp = useRef(0); refDown = useRef(0); } else { refDown = useRef(0); refUp = useRef(0); }
Finally, hooks are not available for class components. While in theory it might have been possible to support hooks for class components' render()
method, it's React's philosophy to keep the state in the class this.state
and use this.setState()
to update it, for class components.
The following resources greatly helped me to understand these hook mechanics:
Posted on September 30, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.