16

My question relates to Javascript mechanisms that make react hooks possible.

Recent development in React allows us to create hooks, ie. for React state, within as simple function like:

function App () {
  const [someVar, setSomeVar] = useState('someVarDefaultValue');
  return (
    <div 
      onClick={() => setSomeVar('newValue')}>{someVar}
    </div>
  );
}

The hook useState returns an array with an accessor and a mutator, and we use them by array decomposition inside our App function.

So under the hood, the hook looks something like (just a pseudocode):

function useState(defaultValue) {
  let value = defaultValue;

  function setValue(val) {
    value = val;
  }

  return [value, setValue];
}

When you try this approach in JS it won't work - value decomposed from array will not update if you use setValue somewhere. Even if you use the value as an object, not a primitive defaultValue.

My question is how does hook mechanism work in JS?

From what I've seen in React sourcecode it uses reducer function and type-checking with Flow. The code is tricky to follow for me to understand the big picture.

This question is not about how to write custom hooks in React.

It's also not question how hooks work under the hood in context of React state management answered in this question: React Hooks - What's happening under the hood?

mtx
  • 1,196
  • 2
  • 17
  • 41
  • What does a `typeof someVar` show? I would assume it is a function. – t.niese Dec 22 '18 at 12:10
  • @t.niese typeof shows the type of value as provided by defalutValue, so number or string etc. I think under the hood the `value` is stored as object obj = { value: defaultValue, [perhaps something else like 'type' prop here] }, so the accessor returns obj.value – mtx Dec 22 '18 at 13:24
  • @EmileBergeron please see updated note in the question, it is not duplicate of post you mention – mtx Nov 20 '19 at 11:51
  • It looks exactly "how it works under the hood" regardless of your new edit. – Emile Bergeron Nov 20 '19 at 15:15
  • @EmileBergerson it is not the same topic, both questions ask for mechanism on react hooks, but this one is about JS and overall progrsmming language features like memoizing and closures. The other question is about particular React implementation of these patterns – mtx Nov 20 '19 at 18:20

2 Answers2

23

The state value has to be stored outside of the useState function, in some internal representation of the component instance, so that it returns persistent results across calls. Additionally setting the value has to cause a rerender on the component it gets called in:

// useState must have a reference to the component it was called in:
let context;

function useState(defaultValue) {
  // Calling useState outside of a component won't work as it needs the context:
  if (!context) {
    throw new Error("Can only be called inside render");
  }

  // Only initialize the context if it wasn't rendered yet (otherwise it would re set the value on a rerender)
  if (!context.value) {
    context.value = defaultValue;
  }

  // Memoize the context to be accessed in setValue
  let memoizedContext = context;

  function setValue(val) {
    memoizedContext.value = val;

    // Rerender, so that calling useState will return the new value
    internalRender(memoizedContext);
  }

  return [context.value, setValue];
}

// A very simplified React mounting logic:
function internalRender(component) {
  context = component;
  component.render();
  context = null;
}

// A very simplified component
var component = {
  render() {
    const [value, update] = useState("it");
    console.log(value);
    setTimeout(update, 1000, "works!");
  }
};

internalRender(component);

Then when setValue gets called, the component rerenders, useState will get called again, and the new value will get returned.

The upper example is very simplified. Here's a few things that React does differently:

  1. The state is not stored in a "context property" but rather in a linked list. Whenever useState is called, the linked list advances to the next node. That's why you should not use hooks in branches/loops.
  2. The setState function gets cached and the same reference gets returned each time.
  3. Rerendering does not happen synchronously.
ggorlen
  • 44,755
  • 7
  • 76
  • 106
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • 1
    how do you know that defaultValue is stored in context.value ? For multiple calls of useState every call would override the context.value... We haven't explictly stated the name of variable in useState, is there any convention for such case? How we keep track which useState refers to which stored value? – mtx Jan 25 '19 at 16:28
  • 1
    @mtx This is just a very simplified demo, meant to be easy to read/understand. In reality it is far more conplex – Jonas Wilms Jan 25 '19 at 19:12
  • 1
    in other words, closure to the rescue.. :) – stackoverflow Apr 09 '19 at 08:20
  • But How this snippet of code return new Component ? Because `update = setValue` and `update -> internalRender -> component.render` its basically a recursion function ! So by calling update return `component` its self ! – Nur Oct 27 '20 at 10:32
  • I mean this code is just fancy way to call its self ! Like `let component = { value: "default", render() { this.value = "new value"; this.render(); } }` – Nur Oct 27 '20 at 10:37
  • beautiful answer Jonas <3 – Mulan Jul 02 '22 at 03:59
0

In the following, multiple calls of useState() simulated by an array for each state variable.

  • In each state updater method call, render will be called by React. Thus, we force render by calling an original state updater method(i.e. setValue) after calling our simulated state updater.

  • Function Component (SimpleForm) will be called for rendering so React will reset the context(not state) for component internally before invoking this method. Thus, we simulate this with a resetContext method.


import React, { useState } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

let myState = {};
let counter = 0;
function resetContext() {
  counter = 0;
}
function myUseState(initialValue) {
  console.log("counter: ", counter, " - myState:", myState);
  const notAlreadyDefined = myState[counter] === undefined;
  if (notAlreadyDefined) {
    myState[counter] = initialValue;
  }
  let cnt = counter;
  const pair = [
    myState[cnt],
    (val) => {
      console.log("setter", val, cnt);
      myState[cnt] = val;
      // In each updater method, render() will be called by React.
      // So, we force render by calling an original state updater method(i.e. setValue) after calling our simulated state updater.
    }
  ];
  counter++;
  return pair;
}

function SimpleForm(props) {
  const [value, setValue] = useState("John");
  const [value2, setValue2] = useState("Edward");

  // Function Component (SimpleForm) will be called to render so React will reset the context(not state) for component internally before invoking this method.
  // So, we simulate this with a resetContext method.
  resetContext();
  const [firstName, setFirstName] = myUseState("John");
  const [lastName, setLastName] = myUseState("Edward");
  const [age, setAge] = useState(30);

  console.log("called", new Date(), firstName, lastName);

  return (
    <form>
      <label>
        First Name:
        <input
          type="text"
          value={firstName}
          onChange={(event) => {
            setValue(event.target.value);
            setFirstName(event.target.value);
          }}
        />
      </label>
      <br />
      <label>
        Last Name:
        <input
          type="text"
          value={lastName}
          onChange={(event) => {
            setValue2(event.target.value);
            setLastName(event.target.value);
          }}
        />
      </label>
      <br />
      <label>
        Age:
        <input
          type="number"
          value={age}
          onChange={(event) => setAge(event.target.value)}
        />
      </label>
      <br />
      <input type="submit" value="Submit" />
    </form>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(
  <>
    <SimpleForm firstName="JOHN" lastName="Edward" age={30} />
    <br />
  </>,
  rootElement
);

https://codesandbox.io/s/react-usestate-hook-example-forked-unjk1m?file=/src/index.js

Also, check the explanation from React Docs

Under the Hood How does React associate Hook calls with components? React keeps track of the currently rendering component. Thanks to the Rules of Hooks, we know that Hooks are only called from React components (or custom Hooks — which are also only called from React components).

There is an internal list of “memory cells” associated with each component. They’re just JavaScript objects where we can put some data. When you call a Hook like useState(), it reads the current cell (or initializes it during the first render), and then moves the pointer to the next one. This is how multiple useState() calls each get independent local state.

https://reactjs.org/docs/hooks-faq.html#under-the-hood

lockedscope
  • 965
  • 17
  • 45