Cover image by Miguel Discart, on Flickr
At ReactConf the team around React presented a new way to implement interactive components with React called hooks.
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:
- 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.
- 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.
- It creates a local variable
savedHooks
to store the state of all hooks. - It calls its local
render
function to do the actual rendering. - The
render
function clears thetarget
DOM element and loops over the array ofcomponents
. -
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 freshMap
object. - The component does its thing, like calling the
useState
function. - The
hookData
stored by our components call touseState
gets a reference to the localrender
function so it can initiate a re-render and itscalls
attribute is reset. - The
globalHooks
data is saved locally for the next run. - The
globalHooks
is set tonull
, if there was a next component it couldn't access our data via theglobalHooks
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:
- It gets a
defaultValue
that should be returned on the first call. - It tries to get its state from the last run from the
globalHooks
variable. This is aMap
object set by ourrun
function before our component function is called. Either it has data from the last run, or we need to create our ownhookData
. - The
hookData.store
array is used to store the values from last calls and thehookData.calls
value is used to keep track of how much this function has been called by our component. - With
hookData.store[hookData.calls]
we can grab the last value stored by this call; if it doesn't exist we have to use thedefaultValue
. - The
setValue
callback is used to update our value, for example when clicking a button. It gets a reference tocalls
so it knows to which call of thesetState
function it belongs. It then uses thehookData.render
callback, supplied by therender
function, to initiate a re-render of all components. - The
hookData.calls
counter is incremented. - The
hookData
is stored in theglobalHooks
variable, so it can be used by therender
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.
My First Book
In the last months I didn't blog as much as before. That's because I wrote a book about learning the basics of React:
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.