react hook

As we all know, React has two forms of components, class components and function components, and developers can use class components and function components to achieve the same purpose and build the exact same page. Since I came across React last August, I have been using function components because they are recommended by my company. I have to say that function components are much better than class components. You can use useEffect to implement componentDidUpdate, componentDidMount and componentWillUnmount of the class component. While marveling at the power of hooks, I’ve often wondered how React actually implements the useEffect and useState hooks? Each time a function component is rendered, the component function is simply executed once, the function is not instance (no this pointer), so how is the state of the component recorded and updated? Maybe you are like me, you will guess that the state of the function component is achieved by closures, then congratulations, your guess is right, hook is the use of the idea of closure. For those who don’t know about closures, please move to MDN to learn about the concept of closures before coming here to continue reading.

This article is about 15 minutes long, so I’m sure you’ll get something out of it. The article focuses on deepening your understanding of how hooks work by manually implementing a mock React yourself. It includes the most commonly used useEffect and useState implementations. I refer to some blogs and materials, and then manually organize their own practice.

I. The Basics

Before mocking the implementation of React, we must understand some concepts.

1. JSX

A JSX is a normal javascript object that Babel translates into a function call called React.createElement(). The following two pieces of code, for example, are equivalent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

// 经过babel转译后
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

React.createElement returns an ordinary javascript Object.

2. Rendering is actually executing a function

Each time a function component re-renders, it actually executes the component’s function once.

For example, the App component below is actually executing App(props) each time it re-renders.

1
2
3
const App = (props) => {
    ...
};

II. Simulating React

1. A simple React

First we implement the simplest React ourselves, with only the render function. render takes a Component as an argument and exampleProps represents the Props passed to the component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const React = {
    render: Component => {
        const exampleProps = {
            unit: "likes"
        };
        const compo = Component(exampleProps);
        compo.render();
        return compo;
    }
}

In order to test this simple version of React, we need a component whose render function outputs some information.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const Component = (props) => {
    return {
        render: () => {
            console.log("render", {
                type: "div",
                inner: `${props.unit}`
            })
        }
    };
}

Then apply it in practice to see the effect.

1
2
let App = React.render(Component); // log: render {type: "div", inner: "likes"}
App = React.render(Component); // log: render {type: "div", inner: "likes"}

We Component rendered twice, and both renders printed log messages.

2. useState

Next, we extend React to implement useState. useState can return a value and a dispatcher that can update it. useState accepts an initial value parameter that sets the initial value of the state.

Returns a stateful value, and a function to update it.

The function component uses useState to create and update the state of the component, which is one of the most common hooks in react. Let’s implement useState ourselves.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const React = {
    index: 0,
    state: [],
    useState: defaultProp => {
        const cachedIndex = React.index;
        if (!React.state[cachedIndex]) {
            React.state[cachedIndex] = defaultProp;
        }
        const currentState = React.state[cachedIndex];
        const currentSetter = newValue => {
            React.state[cachedIndex] = newValue;
        };
        React.index++;

        return [currentState, currentSetter];
    },
    render: Component => {
        const exampleProps = {
            unit: "likes"
        };

        const compo = Component(exampleProps);
        compo.render();

        React.index = 0; // reset index

        return compo;
    }
}

The function of closures is used here. We store the state values into React.state array and then create a currentSetter which is used to update the state values. Finally index is added to store the new state value. Due to the nature of closures, when there are multiple useState values within a component function, each cachedIndex is independent, so the state corresponding to each currentSetter is also independent. Another thing to note is that React.index = 0 must be reset in the render function, because the index must be 0 every time the component function is executed, because the hook is also executed every time.

Update the Component and add state to it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const Component = (props) => {
    const [count, setCount] = React.useState(0);
    const [name, setName] = React.useState("Steve");

    return {
        click: () => setCount(count + 1),
        personArrived: person => setName(person),
        render: () => {
            console.log("render", {
                type: "div",
                inner: `${count} ${props.unit} for ${name}`
            })
        }
    };
}

Test component rendering.

1
2
3
4
5
6
7
8
9
let App = React.render(Component); // log: render {type: "div", inner: "0 likes for Steve"}
App = React.render(Component); // log: render {type: "div", inner: "0 likes for Steve"}

App.click();
App = React.render(Component);  // log: render {type: "div", inner: "1 likes for Steve"}

App.click();
App.personArrived("Peter");
App = React.render(Component); // log: render {type: "div", inner: "2 likes for Peter"}

The useState we simulated functions as expected. It is quite different from the actual useState in React, but it is enough to help us understand it.

3. useEffect

useEffect is executed asynchronously, after the component function is executed. Here’s how the documentation describes useEffect, and I’ve excerpted a few of the most important features.

  1. The function passed to useEffect will run after the render is committed to the screen.
  2. By default, effects run after every completed render, but you can choose to fire them only when certain values have changed.
  3. the function passed to useEffect may return a clean-up function.

The first point means that useEffect is asynchronous, the second point means that we can pass a second parameter (dependency) to useEffect to control whether the first function parameter in useEffect is executed or not, and the third point is that the function we pass can return a function as a cleanup or unsubscribe function.

We will follow the above three features to implement our own useEffect.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const React = {
    index: 0,
    state: [],
    useState: defaultProp => {
        const cachedIndex = React.index;
        if (!React.state[cachedIndex]) {
            React.state[cachedIndex] = defaultProp;
        }

        const currentState = React.state[cachedIndex];
        const currentSetter = newValue => {
            React.state[cachedIndex] = newValue;
        };
        React.index++;

        return [currentState, currentSetter];
    },
    useEffect: (callback, dependencies) => {
        const cachedIndex = React.index;
        const hasChanged = dependencies !== React.state[cachedIndex];
        if (dependencies === undefined || hasChanged) {
            callback();
            React.state[cachedIndex] = dependencies;
        }

        React.index++;
        return () => console.log("unsubscribed effect");
    },
    render: Component => {
        const exampleProps = {
            unit: "likes"
        };

        const compo = Component(exampleProps);
        compo.render();

        React.index = 0; // reset index

        return compo;
    }
}

Store the dependencies in React.state and compare the dependencies with React.state[cachedIndex] (the previously stored dependencies) to determine if the dependency has changed. If it has changed, we execute a callback and update the dependencies stored in React.state for the next execution.

We also update the Component to include the useEffect hook.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const Component = (props) => {
    const [count, setCount] = React.useState(0);
    const [name, setName] = React.useState("Steve");

    const exitThis = React.useEffect(() => {
        console.log("Effect ran");
    }, name);

    return {
        click: () => setCount(count + 1),
        personArrived: person => setName(person),
        unsubscribe: () => exitThis(),
        render: () => {
            console.log("render", {
                type: "div",
                inner: `${count} ${props.unit} for ${name}`
            })
        }
    };
}

The final return object contains an unsubscribe, which is used to simulate the cleanup work of useEffect.

Test the useEffect function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let App = React.render(Component);
// log: Effect ran
//      render {type: "div", inner: "0 likes for Steve"}

App = React.render(Component); // log: render {type: "div", inner: "0 likes for Steve"}

App.click();
App = React.render(Component); // log: render {type: "div", inner: "1 likes for Steve"}

App.click();
App.personArrived("Peter");
App = React.render(Component);
// log: Effect ran
//      render {type: "div", inner: "2 likes for Steve"}

App.unsubscribe(); // log: unsubscribed effect

As you can see, the useEffect function is executed when the component is first loaded, and also when the name is changed, and finally the unsubscribe is called to perform the cleanup.

This is almost the end of the content, you must have a deeper understanding of hooks. There are more similar hooks that you can try to simulate yourselves, such as useCallback.