loading...

Yet another OOP/C# person (me) trying to understand the mechanics behind React Hooks

noseratio profile image Andrew Nosenko Updated on ・4 min read

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, I'd appreciate a comment or a DM on Twitter.

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(countUp + 1);    
    setCountDown(countDown - 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.

  • When a hook like useRef is called, React can tell which component is calling it (the one currently being rendered), so React can retrieve the individual component's state entry it had previously mapped and stored in its static state. That's where the current values of hooks like useRef and useState 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 or useState within the component function matters, and it should remain the same across subsequent renders. In our case, React initially creates two entries for useRef in its internal state for MyComponent, then two entries for useState.

  • Upon subsequent renders (invocations of MyComponent), React knows how to access the correct state and which values to return, by the order of each useRef or useState 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 contrived example will likely screw up the state of refUp and refDown very soon, because their order of useRef 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 by:

noseratio profile

Andrew Nosenko

@noseratio

Dad, a startup founder, ex-Principal Software Engineer at Nuance Communications, he/him.

Discussion

pic
Editor guide