11/22/2018 Β· react

React Hooks Demystified

At ReactConf the team around React presented a new way to implement interactive components with React called hooks.

Cover image by Miguel Discart, on Flickr

They published an RFC so React developers could discuss if this was a good idea.

In this article, we look into how such a functionality could be implemented.

What

Hooks are functions you can call inside your functional components to get the functionality, you would typically only get with component classes.

Why

The basic idea behind hooks is to simplify React development in general, but I won't go into detail, you can read more about it from Dan Abramov, a React core developer, here.

Disclaimer

Read the docs first!

This is an ALPHA feature of React and should not be used in production code.

In this post, we won't use React, but a few lines of code to illustrate how hooks could work.

How

Many people think hooks are magic and go against the philosophy of React and I can't blame them. If we look at the example, it doesn't tell much about what's happening.

import React, {useState} from "react";

function CounterButtton(props) {
  let [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

It uses a simple function call to useState and somehow manages to get us the current state and allows us to change it and rerender the component with the new value.

The JavaScript pros will see the culprit here: globals!

If the useState function doesn't meddle with call stacks to get access to our calling component function, it has to store the data globally.

And if you read Dan's article you may find this tweet:

  1. JavaScript is single threaded, if someone clears the global before calling our hook function, we will write in a fresh global and nobody can do something while our function runs as long as we only make synchronous calls.
  2. React calls our functional component so it has control over what happens before and after that call.

Hooks Example

Below, I’ve tried to write a simple example that illustrates how we could implement the "magic" of hooks. This has nothing to do with the official React implementation, but rather, it shows how the idea works.

First, we have some component definition:

function NumberButton() {
  let [valueA, setValueA] = useState(0);
  let [valueB, setValueB] = useState(100);

  return {
    type: "button",
    props: {
      children: `A:${valueA} B:${valueB}`,
      onClick() {
        setValueA(valueA + 1);
        setValueB(valueB - 1);
      }
    }
  };
}

The NumberButton function calls the useState function, which has the same interface as Reacts useState function.

It returns an object that is the definition of a <button> element with some text and a handler.

The function that renders everything into the DOM looks like this:

function run(components, target) {
  let savedHooks = new Map();
  render();

  function render() {
    target.innerHTML = "";
    components.forEach(function(component) {
      globalHooks = savedHooks.get(component);

      if (!globalHooks) globalHooks = new Map();

      renderToDom(component, target);

      for (let [id, hookData] of globalHooks.entries()) {
        hookData.calls = 0;
        hookData.render = render;
      }

      savedHooks.set(component, globalHooks);

      globalHooks = null;
    });
  }
}

function renderToDom(component, target) {
  let { props, type } = component();

  let element = document.createElement(type);
  element.innerHTML = props.children;
  element.onclick = props.onClick;
  target.appendChild(element);
}

It takes an array of components and a DOM element as a render target.

It can only render flat lists of components, no nesting possible, to keep things simple. It also doesn't do any DOM diffing.

  1. It creates a local variable savedHooks to store the state of all hooks.
  2. It calls its local render function to do the actual rendering.
  3. The render function clears the target DOM element and loops over the array of components.
  4. Here is where the magic happens: The globalHooks variable is overridden right before the component function is used, either with already stored data from the last run or with a fresh Map object.
  5. The component does its thing, like calling the useState function.
  6. The hookData stored by our components call to useState gets a reference to the local render function so it can initiate a re-render and its calls attribute is reset.
  7. The globalHooks data is saved locally for the next run.
  8. The globalHooks is set to null, if there was a next component it couldn't access our data via the globalHooks anymore.

The actual hook function looks like this:

let globalHooks;
function useState(defaultValue) {
  let hookData = globalHooks.get(useState);

  if (!hookData) hookData = { calls: 0, store: [] };

  if (hookData.store[hookData.calls] === undefined)
    hookData.store[hookData.calls] = defaultValue;

  let value = hookData.store[hookData.calls];

  let calls = hookData.calls;
  let setValue = function(newValue) {
    hookData.store[calls] = newValue;
    hookData.render();
  };

  hookData.calls += 1;
  globalHooks.set(useState, hookData);

  return [value, setValue];
}

Let's go through it step-by-step:

  1. It gets a defaultValue that should be returned on the first call.
  2. It tries to get its state from the last run from the globalHooks variable. This is a Map object set by our run function before our component function is called. Either it has data from the last run, or we need to create our own hookData.
  3. The hookData.store array is used to store the values from last calls and the hookData.calls value is used to keep track of how much this function has been called by our component.
  4. With hookData.store[hookData.calls] we can grab the last value stored by this call; if it doesn't exist we have to use the defaultValue.
  5. The setValue callback is used to update our value, for example when clicking a button. It gets a reference to calls so it knows to which call of the setState function it belongs. It then uses the hookData.render callback, supplied by the render function, to initiate a re-render of all components.
  6. The hookData.calls counter is incremented.
  7. The hookData is stored in the globalHooks variable, so it can be used by the render function after the component function returned.

We can run the example like so:

let target = document.getElementById("app");
run([NumberButton], target);

You can find a working implementation with example components on GitHub.

Conclusion

While the approach I took for implementation is far away from the actual React implementation, I think it demonstrates that hooks aren't crazy dev magic, but a smart way to use JavaScript constraints, that you could implement yourself.

This article was also posted on dev.to

My First Book

If you liked this explanation and want to learn React, you may be interested in my first book:

If you like understanding how React works by taking it apart, then you might like my book React From Zero. In the book, I dissect how React works by examining how components work, how elements are rendered, and how to create your own Virtual DOM.

You can download the first chapter for free here.

Kay