Behind the Scenes: React Hooks API

Written by
Daniel Mejia

React Hooks are a new pattern introduced in React 16.8 that lets you use state, side effects, and multiple features using functional components.


import React, { useState } from 'react';
 
function ComponentState() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
       </button>
     </div>
   );
}

With the hooks API, developers face problems with the ‘magic’ behind them. But hooks don’t use any magical library or super fancy programming skills. This article provides a brief introduction of how hooks work under the hood using common JavaScript concepts.

Closures

One of the most important concepts in the JavaScript world is closures. According to MDN, a closure is: 

The combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

They are confusing to many, especially newer JavaScript developers, but we can say that they allow you to keep the state of your variables available for inner functions or modules. Sounds familiar to useState, right? Actually, the internal implementation of this hook uses the concept of closures to track the state in the components. You will see this later in the post.

Let’s simplify this with an example.


function outerFunction() {
 let variableOutside = 'Hello World';
 function innerFunction() {
   console.log(variableInOuterFunction);
 }
 innerFunction();
}

Notice how you can use variableOutside inside innerFunction? That's basically a closure, because the value, in this case, Hello World is available and locked in the nested function. Pretty simple, right? Remember that you can use as many nested levels as you want.

React useState hook

Now let’s try to create a clone of the useState hook by following the same pattern.


function useStateClone(initialValue) {
 let state = initialValue;
 
 function setState(newVal) {
   state = newVal // setting a new value for state
 }
 
 return [state, setState] // exposing state and function for external use
}
 
const [value, setValue] = useStateClone(1) // using array destructuring
console.log(value) // logs 1 - the initialValue we gave
setValue(2) // sets state inside useState's scope
console.log(value) // logs 1, Still the same

Looks like the value is still the same. This is one form of the stale closure problem. When we destructure value from the output of useStateClone, it refers to the state as of the initial state call and never changes again. This is not the expected functionality that we want; we need our state to reflect the current state even when we update it.

This can be easily fixed, by moving our state inside another closure to keep tracking the state and the changes for it.


function ReactClone() {
  let state;
 
  function render(component) {
   return component()
  }
 
  function useStateClone(initialValue) {
    state = state || initialValue;
    function setState(newVal) {
      if (typeof newVal === 'function') {
        state = newVal(state); // Allow to set state using a function
      } else {
        state = newVal
      }
    }
   
    return [state, setState]
  }
 
  return {
   render,
   useStateClone,
  }
}
 const React = ReactClone();
 
 function MyComponent() {
   const [value, setValue] = React.useStateClone(0)
   console.log(value) // logs 0, 1, 2
   setValue((prev) => prev + 1) // sets state inside useStateClone's scope
 }
 
 // Simulate react re-renders
 React.render(MyComponent);
 React.render(MyComponent);
 React.render(MyComponent);
 

Here we create a main closure by defining the ReactClone module. This module, like React, stores and keeps tracking the component state. In this example, we are just tracking a single state for a single component using the variable state

We also implemented the render function to simulate the react re-renders when making changes to the application as normally React does. 

Now the useStateClone hook is defined internally in the ReactClone closure and since we define our state outside of it we can now read it and track it. Notice how we finished the implementation of useStateClone to follow the same interface defined by React, allowing now to update the state by setting a new value or providing a function that calculates the new value by using the old one.

You can use the clone and now effectively see the changes in real-time as you want. Obviously, the React implementation contains more logic and communication with the React reconciler in order to update our components automatically by changing the state but this is the simplest way to get the same feature without any dependency.

React useEffect hook

In this section, we are going to add a new hook for our clone to provide the useEffect functionality.


function ReactClone() {
  let state;
  let storedDependencies;
  
  function render(component) {
    return component()
  }
  
  function useEffectClone(effect, effectDependencies = null) {
    const hasNoDependencies = !effectDependencies; // Calling effect without dependencies.
    const firstExecution = !storedDependencies; // Calling effect the very first time.
    const hasChangeSome = storedDependencies?.some((dep, i) => dep !== effectDependencies[i]); // Calling effect with some a different value
 
    if (hasNoDependencies || firstExecution || hasChangeSome) {
      effect()
      storedDependencies = effectDependencies
    }
  }
 
  function useStateClone(initialValue) {
    state = state || initialValue;
    function setState(newVal) {
      if (typeof newVal === 'function') {
        state = newVal(state); // Allow to set state using a function as param
     } else {
       state = newVal
     }
   }
   return [state, setState] // exposing state and function for external use
 }
  
  return {
    render,
    useStateClone,
    useEffectClone,
  }
}
 
const React = ReactClone();
 
function MyComponent() {
  const [value, setValue] = React.useStateClone(0);
   React.useEffectClone(() => {
     console.log(value) // logs 0, 1 even when render three times
   }, [value]);
 
  setValue(1)
}
 
// Simulate react re-renders
React.render(MyComponent);
React.render(MyComponent);
React.render(MyComponent);

Let’s break it into parts. First, we need to define the interface, so it’s okay to receive effect a function that can be executed for side effects in the component, and also an optional effectDependencies array of variables that would make you effect re-run. 

To be able to execute the effect, we need to define a storedDependencies variable in the outer closure to keep tracking the changes and in that way decide whether or not to run the effect. Now, we have four different scenarios:

The first one is when we are rendering our component for the very first time. In this case, we are always going to execute our function at least once.

The second one is the case where we only provide the effect without a dependencies array. In this case, hasNotDependencies will always be true and our effect will be called each time the component re-renders matching the exact behavior for the real hook.

The third case will allow us to receive an empty array of dependencies to execute the effect only once no matter if the component re-render multiple times. This is because the function is going to execute it the first time and the storedDependencies will be an empty array for the coming re-renders and hasSomeChanges will be always false since the stored array and the effect one will be basically the same.

The last case is when you provide a variable into the effectDependencies, so in this case, the effect will be executed the first time but when the component re-renders this will be only called again if a new value for the array of dependencies is different from the last render because we are always comparing the last dependencies stored with the new values provided in each render. So the new values become the old ones, and so on.

The reader easily can realize how this implementation has more complexity, but it’s still using only common JavaScript and applying the right conditions in the right way. 

You can also notice that we are keeping state and storedDependencies in the ReactClone module to keep track of the changes for useStateClone and useEffectClone. But what if I want to have multiple states or run several side effects? Even better, what if I want to have states and effects in multiple components?

Calling hooks multiple times


function ReactClone() {
  const hooks = [];
  let hookBeingExecuted = 0;
 
  function render(component) {
    const componentContent =  component();
    hookBeingExecuted = 0; // Reset hooks index each render
    return componentContent;
  }
 
  function useEffectClone(effect, effectDependencies) {
   const myPosition = hookBeingExecuted; // Cloning global index to lock the value for current state.
 
   const hasNoDependencies = !effectDependencies;
   const firstExecution = !hooks[myPosition];
   const hasChangeSome = hooks[myPosition]?.some((dep, i) => dep !== effectDependencies[i]);
 
   if (hasNoDependencies || firstExecution || hasChangeSome) {
     effect()
     hooks[myPosition] = effectDependencies
   }
 
   hookBeingExecuted += 1; // Update index for the next hook execution.
 }
 
  function useStateClone(initialValue) {
    const myPosition = hookBeingExecuted; // Cloning global index to lock the value for current state.
    hooks[myPosition] = hooks[myPosition] || initialValue;
 
    function setState(newVal) {
      if (typeof newVal === 'function') {
        hooks[myPosition] = newVal(hooks[myPosition]);
      } else {
        hooks[myPosition] = newVal
      }
    }
  
    hookBeingExecuted += 1; // Update index for the next hook execution.
    return [hooks[myPosition], setState]
  }
 
  return {
   render,
   useStateClone,
   useEffectClone,
  }
}
 
const React = ReactClone();
 
function MyComponent() {
  const [value, setValue] = React.useStateClone(0)
  const [name, setName] = React.useStateClone('Daniel')
 
  React.useEffectClone(() => {
    console.log(name) // logs daniel
  }, []);
 
  React.useEffectClone(() => {
    console.log(value) // logs 0, 1, 2
  }, [value]);
 
   React.useEffectClone(() => {
     console.log('every') // logs every, every, every
   });
 
   setValue(pre => pre + 1);
   setName('Laura')
}
 
 
// Simulate react re-renders
React.render(MyComponent);
React.render(MyComponent);
React.render(MyComponent);

Let’s see how we now keep a hooks array to store whether the state for a useStateClone hook or the dependencies for the useEffectClone hook. Using this pattern we can have as many hooks as we want. In order to follow the execution, we are going to store hookBeingExecuted initialized always to 0. Notice also that for each render we want to reset the value to 0 again to start the process again over and over. 

The useStateClone implementation is now a little bit different since now we need to get the position for the current state hook being executed and stored in a myPosition variable to create a closure for setState  function. This is because we are going to use setState function after the definition of the hooks, and since the hookBeingExecuted is global we will probably update the state of a different one. For that reason, each hook needs to create a closure for its position no matter the global state. Finally, we need to update hookBeingExecuted for the next hook and so on.

In the case of useEffectClone, the change is pretty simple too. We just need to create the closure for the current position, which is pointing to the dependencies for it and at the end just increments the hookBeingExecuted for the next hook.

Conclusion

After reviewing the implementation you can see that there is no magic and no libraries here. Now you can also better understand the rules defined by React about hooks. For example, let’s review one: Don’t call Hooks inside loops, conditions, or nested functions. Makes sense, right? We are using an array and an index value to track the execution of the hook. But if you use it conditionally, you can’t know the current position inside of that array. That’s because they need to be executed in the same order as defined in the component.

The reader can also try to complete the implementation with other hooks like useRefuseReducer. Also, notice that the code examples were simply because we wanted to explain the concepts behind them, but the real React implementation has a lot of more details, conditions, and a connection with the entire system in order to provide all the features.