Come on, touch and write a hook

Come on, touch and write a hook

hello, this is Xiaochen. Today, I will take you to write a mini version of hooks to facilitate you to understand the operation mechanism of hook in the source code. It is equipped with diagrams and nanny level tutorials. Just ask the students for a little help

Step 1: introduce React and React DOM

Because we want to transform jsx into virtual DOM, this step is left to babel. After jsx is parsed by babel, a call to React.createElement() will be formed, and the return result after the execution of React.createElement() is jsx object or virtual dom.

Because we want to render our demo to dom, we introduce ReactDOM.

import React from "react";
import ReactDOM from "react-dom";

Step 2: let's write a small demo

We define two states, count and age, which trigger the update when clicking, and increase their values by 1.

In the source code, the useState is saved on a Dispatcher object, and different hooks are obtained during mount and update, so let's get the useState from the Dispatcher temporarily and define the Dispatcher later.

Next, define a schedule function, which will re render the components each time it is called.

function App() {
  let [count, setCount] = Dispatcher.useState(1);
  let [age, setAge] = Dispatcher.useState(10);
  return (
    <>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(() => count + 1)}> Add count</button>
      <p>Age is {age}</p>
      <button onClick={() => setAge(() => age + 1)}> Add age</button>
    </>
  );
}

function schedule() { //Each call re renders the component
  ReactDOM.render(<App />, document.querySelector("#root"));
}

schedule();

Step 3: define Dispatcher

Before looking at this part, first clarify the relationship between fiber, hook and update. Look at the figure:

image-20211129105128673

What is a Dispatcher: a Dispatcher is an object in the source code, on which various hooks are stored. Different dispatchers have been used in mount and update. Let's see what a Dispatcher looks like in the source code:

After calling useState, a resolveDispatcher function will be called. After calling this function, a dispatcher object will be returned, which has hooks such as useState.

image-20211126164214374

Let's see what this function does. This function is relatively simple. We get the current directly from the ReactCurrentDispatcher object, and then the returned current is the dispatcher. What is this ReactCurrentDispatcher? Don't worry, continue to look in the source code.

image-20211126164903336

There is such a piece of code in the source code. If it is in a formal environment, it can be divided into two cases

  1. If current = = = null | | current.memorizedstate = = = null is satisfied, it means that we are in the first rendering, that is, when we mount, where current is our fiber node, and the memorizedstate saves the hook on the fiber, that is, when we apply the first rendering, the current fiber does not exist, and we have not created any fiber node, Or there are some fibers, but the corresponding hook is not built. At this time, it can be considered that it is in the first rendering, and we get HooksDispatcherOnMount
  2. If current = = = null | current.memoizedstate = = = null is not satisfied, it means that we are in the update stage, that is, when updating, we get HooksDispatcherOnUpdate
if (__DEV__) {
    if (current !== null && current.memoizedState !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
    } else if (hookTypesDev !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
    }
  } else {
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }

Let's take a look at the HooksDispatcherOnMount and HooksDispatcherOnUpdate. Good guy, it turns out that you include all hooks.

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useOpaqueIdentifier: mountOpaqueIdentifier,

  unstable_isNewReconciler: enableNewReconciler,
};

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useOpaqueIdentifier: updateOpaqueIdentifier,

  unstable_isNewReconciler: enableNewReconciler,
};

Therefore, dispatcher is an object that contains all hooks. Different dispatchers are obtained when rendering and updating for the first time. Different functions will be called when calling hooks. For example, if useState is used, mountState will be called when mounting, and update state will be called when updating.

image-20211126170906166

Now let's write the dispatcher. The dispatcher is an object. There is a useState on the object. We use a self executing function to represent it. In addition, we also need to use two variables and a constant fiber

  • workInProgressHook indicates the traversed hook (because the hook will be saved in the linked list, you need to traverse the linked list to calculate the saved state on the hook)
  • For simplicity, when defining an isMount=true for mount, set it to false when updating,
  • For simplicity, a fiber is defined as an object. The memorizedstate represents the hook linked list stored on the fiber node, and the stateNode is the demo of the second step.
let workInProgressHook;//hook in current work
let isMount = true;//Whether to mount

const fiber = {//fiber node
  memoizedState: null,//hook linked list
  stateNode: App
};

const Dispatcher = (() => {//Dispatcher object
  function useState(){
    //. . . 
  }

  return {
    useState
  };
})();

Before defining useState, let's first look at the data structures of hook and update

hook:
  • queue: there is a pending attribute on it. Pending is also a circular linked list, which stores updates that have not been updated, that is, these updates will be connected into a circular linked list with the next pointer.
  • memoizedState represents the current state
  • Next: point to the next hook to form a linked list
 const hook = {//Build hook
   queue: {
     pending: null//Unexecuted update linked list
   },
   memoizedState: null,//Current state
   next: null//Next hook
 };
update:
  • action: is the function to start the update
  • Next: connect the next update to form a circular linked list
 const update = {//Build update
    action,
    next: null
  };

Next, define useState in three parts:

  • Create or get a hook:
    1. During mount: call mountWorkInProgressHook to create an initial hook and assign the initial value initialState passed in from useState
    2. During update: call updateWorkInProgressHook to get the hook currently working
  • Calculate the status not updated on the hook: traverse the pending linked list on the hook, call the action function on the linked list node, generate a new status, and then update the status on the hook.
  • Return the new status and dispatchAction, and pass in the queue parameter
function useState(initialState) {
   //Step 1: create or get a hook
    let hook;
    if (isMount) {
      hook = mountWorkInProgressHook();
      hook.memoizedState = initialState;//Initial state
    } else {
      hook = updateWorkInProgressHook();
    }
  //Step 2: calculate the status not updated on the hook
    let baseState = hook.memoizedState;//Initial state
    if (hook.queue.pending) {
      let firstUpdate = hook.queue.pending.next;//First update

      do {
        const action = firstUpdate.action;
        baseState = action(baseState);//Call action to calculate the new state
        firstUpdate = firstUpdate.next;//Calculate the state through the action of update
      } while (firstUpdate !== hook.queue.pending);//Loop when the linked list has not been traversed

      hook.queue.pending = null;//Reset update linked list
    }
    hook.memoizedState = baseState;//Assign a new state
  
  //Step 3: return the new status and dispatchAction, and pass in the queue parameter
    return [baseState, dispatchAction.bind(null, hook.queue)];//Return of useState
  }

Next, define the mountWorkInProgressHook and updateWorkInProgressHook functions

  • mountWorkInProgressHook: called at the time of mount to create a new hook object,
    1. If there is no memoizedState in the current fiber, the current hook is the first hook on the fiber. Assign the hook to fiber.memoizedState
    2. If there is a memoizedState in the current fiber, connect the current hook to workInProgressHook.next.
    3. Assign the current hook to workInProgressHook
  • Updateworkinprogress hook: called during update, returns the current hook, that is, workinprogress hook, and points the workinprogress hook to the next in the hook chain list.
function mountWorkInProgressHook() {//Called on mount
    const hook = {//Build hook
      queue: {
        pending: null//Unexecuted update linked list
      },
      memoizedState: null,//Current state
      next: null//Next hook
    };
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;//The first hook is directly assigned to fiber.memoizedState
    } else {
      workInProgressHook.next = hook;//If it is not the first one, it will be added to the back of a hook to form a linked list
    }
    workInProgressHook = hook;//Record the hook of the current work
    return workInProgressHook;
  }

function updateWorkInProgressHook() {//Called on update
  let curHook = workInProgressHook;
  workInProgressHook = workInProgressHook.next;//Next hook
  return curHook;
}

Step 4: define the dispatchAction

  • Create an update and mount it on queue.pending
    1. If the previous queue.pending does not exist, the created update is the first one, then update.next = update
    2. If the previous queue.pending exists, add the created update to the ring linked list of queue.pending

1

  • Let isMount=false, assign workInProgressHook, and call schedule to update rendering
function dispatchAction(queue, action) {//Trigger update
  const update = {//Build update
    action,
    next: null
  };
  if (queue.pending === null) {
    update.next = update;//Circular linked list of update
  } else {
    update.next = queue.pending.next;//The next of the new update points to the previous update
    queue.pending.next = update;//The next of the previous update points to the new update
  }
  queue.pending = update;//Update queue.pending

  isMount = false;//Flag mount end
  workInProgressHook = fiber.memoizedState;//Update workInProgressHook
  schedule();//Scheduling update
}

Final code

import React from "react";
import ReactDOM from "react-dom";

let workInProgressHook;//hook in current work
let isMount = true;//Whether to mount

const fiber = {//fiber node
  memoizedState: null,//hook linked list
  stateNode: App//dom
};

const Dispatcher = (() => {//Dispatcher object
  function mountWorkInProgressHook() {//Called on mount
    const hook = {//Build hook
      queue: {
        pending: null//Unexecuted update linked list
      },
      memoizedState: null,//Current state
      next: null//Next hook
    };
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;//The first hook is directly assigned to fiber.memoizedState
    } else {
      workInProgressHook.next = hook;//If it is not the first one, it will be added to the back of a hook to form a linked list
    }
    workInProgressHook = hook;//Record the hook of the current work
    return workInProgressHook;
  }
  function updateWorkInProgressHook() {//Called on update
    let curHook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;//Next hook
    return curHook;
  }
  function useState(initialState) {
    let hook;
    if (isMount) {
      hook = mountWorkInProgressHook();
      hook.memoizedState = initialState;//Initial state
    } else {
      hook = updateWorkInProgressHook();
    }

    let baseState = hook.memoizedState;//Initial state
    if (hook.queue.pending) {
      let firstUpdate = hook.queue.pending.next;//First update

      do {
        const action = firstUpdate.action;
        baseState = action(baseState);
        firstUpdate = firstUpdate.next;//Circular update linked list
      } while (firstUpdate !== hook.queue.pending);//Calculate the state through the action of update

      hook.queue.pending = null;//Reset update linked list
    }
    hook.memoizedState = baseState;//Assign a new state

    return [baseState, dispatchAction.bind(null, hook.queue)];//Return of useState
  }

  return {
    useState
  };
})();

function dispatchAction(queue, action) {//Trigger update
  const update = {//Build update
    action,
    next: null
  };
  if (queue.pending === null) {
    update.next = update;//Circular linked list of update
  } else {
    update.next = queue.pending.next;//The next of the new update points to the previous update
    queue.pending.next = update;//The next of the previous update points to the new update
  }
  queue.pending = update;//Update queue.pending

  isMount = false;//Flag mount end
  workInProgressHook = fiber.memoizedState;//Update workInProgressHook
  schedule();//Scheduling update
}

function App() {
  let [count, setCount] = Dispatcher.useState(1);
  let [age, setAge] = Dispatcher.useState(10);
  return (
    <>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(() => count + 1)}> Add count</button>
      <p>Age is {age}</p>
      <button onClick={() => setAge(() => age + 1)}> Add age</button>
    </>
  );
}

function schedule() {
  ReactDOM.render(<App />, document.querySelector("#root"));
}

schedule();

Preview effect: https://codesandbox.io/s/custom-hook-tyf19?file=/src/index.js

Video Explanation (efficient learning): Click to learn

Added by Ben Cleary on Thu, 09 Dec 2021 08:57:31 +0200