Disclaimer: The source code for this article is [email protected]

Hook related terms

hook

React exposes apis to developers on the top-level namespace, such as the following code snippet:

import React , { useState, useReducer, useEffect } from 'react'
Copy the code

We call useState,useReducer, useEffect and so on “hooks”. Specifically, a hook is a javascript function.

Note that when we use the term “hook” below, we have explicitly distinguished ourselves from the term “hook object”.

React has the following built-in hooks:

/* react/packages/react-reconciler/src/ReactFiberHooks.new.js */
export type HookType =
  | 'useState'
  | 'useReducer'
  | 'useContext'
  | 'useRef'
  | 'useEffect'
  | 'useLayoutEffect'
  | 'useCallback'
  | 'useMemo'
  | 'useImperativeHandle'
  | 'useDebugValue'
  | 'useDeferredValue'
  | 'useTransition'
  | 'useMutableSource'
  | 'useOpaqueIdentifier';
Copy the code

Hook object

/* react/packages/react-reconciler/src/ReactFiberHooks.new.js */
export type Hook = {
  memoizedState: any, 
  baseState: any, 
  baseQueue: Update<any, any> | null.queue: UpdateQueue<any, any> | null.next: Hook | null
};
Copy the code

A hook object is a “plain javascript object” from a data type perspective. From the point of view of data structure, it is a one-way linked list (hereinafter referred to as “hook chain”). The value of the next field supports this.

Here is a brief explanation of what each field means:

  • MemoizedState. By going through ithook.queueThe latest value calculated by the circular unidirectional linked list. This value is rendered to the screen during the COMMIT phase.
  • BaseState. The initial value passed in when we call hook. It is the baseline against which new values are computed.
  • BaseQueue.
  • The queue. See the queue object below.

The update object

// react/packages/react-reconciler/src/ReactFiberHooks.new.js 
type Update<S, A> = {
  // TODO: Temporary field. Will remove this by storing a map of
  // transition -> start time on the root.
  eventTime: number,
  lane: Lane,
  suspenseConfig: null | SuspenseConfig,
  action: A,
  eagerReducer: ((S, A) = > S) | null.eagerState: S | null.next: Update<S, A>, priority? : ReactPriorityLevel, };Copy the code

We only need to focus on fields related to the Hook principle, so the type of update object can be simplified as:

// react/packages/react-reconciler/src/ReactFiberHooks.new.js 
type Update<S, A> = {
  action: A,
  next: Update<S, A>
};
Copy the code
  • The action. A term dedicated to useState,useReducer hooks. Because these two hooks are the product of borrowing the concept of Redux, many terms of Redux are used in the internal implementation source code of these two hooks: “dispatch”, “Reducer”, “state”, “action”, etc. But the ation here is not exactly the same as redux’s action. Suppose we have the following code:
const [count,setState] = useState(0);
const [useInfo,dispatch] = useReducer(reducer,{name:'shark uncle'.age:0})
Copy the code

From a source code perspective, we call setState(1), setState(count=> count+1), dispatch({foo:’bar’}) and pass in the argument “action”. For redux actions, {type:string,payload:any} is the convention, but the action in an Update object can be of any data type. For example, the 1 above,count=> count+1 and {foo:’bar’} are actions for update objects.

Also mentioned is the term “dispatch method.” From a source point of view, the second element of the array returned by the useState/useReducer hook calls is actually a reference to the internal dispatchAction instance. Use react.usestate () as an example.

  // react.usestate () is implemented in the mount phase
  function mountState(initialState) {
    // A lot of code is omitted here
    / /...
    var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
    return [hook.memoizedState, dispatch];
  }
  
  // react.usestate () is implemented in the update phase
   function updateState(initialState) {
    return updateReducer(basicStateReducer);
  }
  
  function updateReducer(reducer, initialArg, init) {
    var hook = updateWorkInProgressHook();
    var queue = hook.queue;
    // A lot of code is omitted here
    / /...
    var dispatch = queue.dispatch;
    return [hook.memoizedState, dispatch];
  }
Copy the code

As you can see, all we developers get is a reference to the Dispatch method. Therefore, the second element of the array returned by the useState/useReducer hook calls is collectively referred to as the “dispatch method” below. Calling the Dispatch method causes the Function Component to be rerendered.

  • Next. Pointer to the next Update object. From this we can tell that the Update object is a one-way list. When it becomes a one-way linked list will be discussed later.

The queue object

// react/packages/react-reconciler/src/ReactFiberHooks.new.js 
type UpdateQueue<S, A> = {
  pending: Update<S, A> | null.dispatch: (A= > mixed) | null.lastRenderedReducer: ((S, A) = > S) | null.lastRenderedState: S | null};Copy the code
  • Pending. We call the Dispatch method to do two things: 1) generate a circular one-way linked list of Update objects; 2) Trigger the react scheduling process. Pending is the head pointer to the looping list.
  • Dispatch. The function instance reference to trigger the component re-render is returned to the developer.
  • LastRenderedReducer. Reducer used in the last update phase.
  • LastRenderedState. The state calculated using lastRenderedReducer and rendered to the screen.

currentlyRenderingFiber

This is a global variable that exists in the function Component lifecycle. As the name suggests, this is a Fiber node. Each React Component has a corresponding Fiber node. Fiber nodes are classified by state into two types: Work-in-Progress Fiber and waster-work Fiber. The former represents the current Render phase updating the React Component, while the latter represents the current screen displaying the React Component. These two fiber nodes recycle references to each other via alternate fields. There are source comments as proof:

// react/packages/react-reconciler/src/ReactInternalTypes.js
export type Fiber = {
  / /...
  // A lot of code was omitted
  
  // This is a pooled version of a Fiber. Every fiber that gets updated will
  // eventually have a pair. There are cases when we can clean up pairs to save
  // memory if we need to.
  alternate: Fiber | null.// A lot of code is omitted after that
  / /...

};
Copy the code

The currentlyRenderingFiber here belongs to “work-in-progress fiber”. But to avoid ambiguity, the internal source code uses the current name:

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber = (null: any);
Copy the code

Assignment to this global variable occurs before the function Component is called. Source code as proof:

export function renderWithHooks<Props.SecondArg> (
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
) :any {
	currentlyRenderingFiber = workInProgress;
    // A lot of code is omitted here
    / /...
    let children = Component(props, secondArg);
    // A lot of code is omitted after that
    / /...
}
Copy the code

Yes, the Component here is what we normally call a function Component. CurrentlyRenderingFiber is null before the Function Component is called, it is assigned to the corresponding Fiber node of the Function Component.

currentHook

This is a global variable that corresponds to the hook object already traversed on the old hook chain (the old hook chain was created in the mount phase of the hook). The hook object currently being iterated is stored in the nextCurrentHook local variable in the updateWorkInProgressHook() method.

workInProgressHook

Both the mount and update phases exist. Mount phase, which is a brand new javascript object; Update phase, which is a shallow copy of the old hook object that corresponds to the current called hook. Either the mount or update phase refers to the last hook object in the current hook chain to be processed (the mount phase corresponds to the last hook object created; Update phase, corresponding to the last copied hook object).

The mount stage of hook

Equivalent to the component’s first mount phase, or more specifically the first time a function Component is called (because function Component is essentially a function). In general, this is triggered by reactdom.render ().

Update phase of hook

Equivalent to the component update phase, or more specifically the function Component’s second, third…… The NTH time is called. In general, there are two situations that cause a hook to enter the update phase. In the first case, the update of the parent causes the passive update of the function Component. The second is an update caused by a manual call to the hook’s dispatch method inside the function Component.

summary

From the point of view of data types, the “XXX objects” mentioned above are corresponding data structures from the point of view of data structures. Below, we connect the above mentioned data structures together and then mount the data structures involved in hook:

A few facts

1. The hook called in the mount stage and the hook called in the Update stage are not the same hook

import React, { useState } from 'react';

function Counter(){
	const [count,setState] = useState();
    
    return <div>Now the count is {count}<div>
}
Copy the code

The above code, useSate hook as an example. The Counter function is called repeatedly, and the first time it is called is the “mount phase” of the useState hook. Each subsequent call to the Counter function is the “update phase” of useState.

One of the more subversive facts is that the first call to useState is not the same function as the subsequent call to useState. This is probably not what many people think. UseState is just a reference, the mount phase points to the “mountState” function in the mount phase, and the update phase points to the “updateState” function in the Update phase, which is the implementation detail behind the fact. Look at the source code. The React package exposes useState to developers, which corresponds to the following implementation:

// react/packages/react/src/ReactHooks.js

export function useState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
Copy the code

The resolveDispatcher() function is implemented like this:

function resolveDispatcher() {
  constdispatcher = ReactCurrentDispatcher.current; invariant( dispatcher ! = =null.'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',);return dispatcher;
}
Copy the code

ReactCurrentDispatcher. Current the initial value is null:

// react/src/ReactCurrentDispatcher.js

/** * Keeps track of the current dispatcher. */
const ReactCurrentDispatcher = {
  / * * *@internal
   * @type {ReactComponent}* /
  current: (null: null | Dispatcher),
};
Copy the code

So when is it assigned? What value is assigned to it? The answer is before the function Component is called. Inside the function renderWithHooks(), there is code like this:

export function renderWithHooks<Props.SecondArg> (
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
) :any {
	// A lot of code is omitted here....
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
        
  let children = Component(props, secondArg);
  // A lot of code is omitted here.....
  return children;
}
Copy the code

Here, current is a fiber node. It can be seen from this judgment that when the function Component has no corresponding Fiber node or there is no hook linked list on the fiber node, it is the mount stage of hook. Mount phase, dispatcher. current points to HooksDispatcherOnMount; Otherwise, it is the UPDTE phase. In the update phase, dispatcher. current points to HooksDispatcherOnUpdate.

Finally, we locate the HooksDispatcherOnMount and HooksDispatcherOnUpdate objects respectively, and the truth is clear:

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState, // Focus your attention on this line
  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, // Focus your attention on this line
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useOpaqueIdentifier: updateOpaqueIdentifier,
  unstable_isNewReconciler: enableNewReconciler,
};
Copy the code

UseState corresponds to the “mountState” function in the mount phase. The corresponding function in the update phase is “updateState”. Again, this is just an example of the hook useState, and other hooks are the same, so I won’t repeat it here.

2. UseState () is a simplified version of useReducer().

Compared to useReducer, useState hook is only different in API parameters. In the internal implementation, useState is also a set of useReducer mechanism. Specifically, useState also has its reducer, which is called basicStateReducer in the source code. See the mount phase of the useState implementation:

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

function mountState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  / /...
  const queue = (hook.queue = {
    pending: null.dispatch: null.lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  / /...
}
Copy the code

As can be seen, useState() also has a reducer, which is mounted on the lastRenderedReducer field. What does the basicStateReducer look like?

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

function basicStateReducer<S> (state: S, action: BasicStateAction<S>) :S {
  return typeof action === 'function' ? action(state) : action;
}
Copy the code

As can be seen, this basicStateReducer has the same function signature as the reducer (redux-type) we wrote ourselves :(state,action) => state, which is also a real reducer. In other words, in the mount stage, the Reducer used by useReducer is the reducer passed in by the developer, while useState uses the basicStateReducer formed by React to help us encapsulate actions.

UseState in the mount phase is associated with the useReducer in the update phase. UseState refers to updateState in the update phase, so let’s look at its source code implementation:

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

function updateState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  return updateReducer(basicStateReducer, (initialState: any));
}
Copy the code

UpdateReducer () calls the updateReducer() function, and useReducer references the updateReducer function in the update phase. This is the end of the argument for the fact.

3. The first parameter “Reducer” of useReducer() can be changed

UpdateReducer = updateReducer = updateReducer = updateReducer

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

function updateReducer(reducer,initialArg,init) {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // Get the end of the updated list
  const last = queue.pending;

  // Get the original update object, remember that this is a circular listfirst = last ! = =null ? last.next : null;

  if(first ! = =null) {
    let newState = hook.baseState;
    let update = first;
    do {
      // Perform each update to update the status
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while(update ! = =null&& update ! == first); hook.memoizedState = newState; }const dispatch = queue.dispatch;
  // Returns the latest status and the method to modify the status
  return [hook.memoizedState, dispatch];
}
Copy the code

It can be seen that the Reducer sent by useReducer in the update phase is used for the latest value calculation. That is to say, in the update phase, we can switch reducer according to certain conditions. We don’t do this in real development, but we can do it in the source code.

Perhaps, you ask, could useState do the same? The answer is “no”. Because the reducer used by useState is not within our control. In the internal source code, this reducer is fixed as a basicStateReducer.

The basic principle of hook operation

The whole life cycle of hook can be divided into three stages:

  • Mount stage
  • Trigger update phase
  • The update phase

By understanding what a hook does in these three stages, we can basically understand the basic principles of hook operation.

Mount stage

In simple terms, in the mount phase, we call hook (regardless of type) every time. For example, if I called useState() three times in a row, I would say it was three hook calls), three things would actually happen:

  1. Create a new hook object.
  2. Construct hook single linked list;
  3. Add information about hook objects.

Step 1 and Step 2: [Create a new Hook object] and [Build a single linked list of hooks]

Every time we call a hook, react internally calls the mountWorkInProgressHook() method. The creation of hook objects and the creation of linked lists occur in this method. Because their implementations are all in the same method, here we put them together:

function mountWorkInProgressHook() :Hook {
  const hook: Hook = {
    memoizedState: null.baseState: null.baseQueue: null.queue: null.next: null};if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
Copy the code

Obviously, the variable hook is the original hook object. So we’re done with creating a new hook object.

Next, let’s look at the second step – [build hook singly linked list]. The last few lines of the mountWorkInProgressHook method are:

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
Copy the code

As discussed in the terminology section, the workInProgressHook is a global variable that points to the last hook object to be generated. If the workInProgressHook is null, it means that no hook object was generated at all. If the workInProgressHook is null, it will become the header of the table. Cut a pointer points to currentlyRenderingFiber. MemoizedState 】 【; Otherwise, the currently created hook object is appended to the end of the list. Here, the react internal implementation uses a more subtle implementation. It creates a new pointer to the end of the hook list after each round of building. So, the next time we append a hook, we just need to append the new hook to the workInProgressHook. In fact, the above code can be disassembled like this:

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState  = hook;
    workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook.next = hook; // Why does an assignment to workInProgreshook. next function as an append list? This requires the knowledge of passing by reference to understand.
    workInProgressHook = hook;
  }
Copy the code

The advantage of this approach is that you don’t have to walk through the list to find the last element so that a new element can be inserted later (O(n)). Instead, insert it directly after the workInProgressHook element (O(1)). We need to know that a regular list tail insert looks like this:

   if (currentlyRenderingFiber.memoizedState === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState  = hook;
  } else {
   let currrent = currentlyRenderingFiber.memoizedState;
   while(currrent.next ! = =null){
   	currrent = currrent.next
   }
   currrent.next = hook;
  }
Copy the code

In terms of time complexity, it’s going to reduce the time complexity of the linked list insertion algorithm from O(n) to O(1). Okay, so it’s a little bit expanded. Here we have seen how steps 1 and 2 are implemented in the React source code.

Step 3: Add information about the hook object

The hook object we created in the first step has a number of fields with initial values of NULL. So in the third part, we’re going to populate the values of these fields. The code for these operations is in the mountState function:

function mountState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null.dispatch: null.lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}
Copy the code

Padding for memoizedState and baseState:

 if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
Copy the code

Populate the queue field:

  const queue = (hook.queue = {
    pending: null.dispatch: null.lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
Copy the code

The next field is actually populated in step 2 [Build a hook singly linked list], so I won’t go into details here.

This is how a hook object populates field information. However, it is worth noting that this is also where the queue object of the hook object is initialized and populated. For example, the Dispatch field, lastRenderedReducer and lastRenderedState fields. It is important to mention the dispatch method:

 const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
Copy the code

As you can see from the code above, the dispatch method we get is essentially a reference. It points to the function instance returned by the dispatchAction method via function corrification. The essence of a function’s Currization is closure. By enclosing the currentlyRenderingFiber and Queue variables, React ensures that the corresponding Queue object and currentlyRenderingFiber are accessed when we call the Dispatch method.

Ok, that’s what happens in the mount phase of a hook.

Trigger update phase

When the user invokes the Dispatch method, we enter the update trigger phase. The React source code does not have this concept, but I introduced it to help understand how hook works.

To see what happens when the update phase is triggered, we need only look at the implementation of the dispatchAction method. However, the dispatchAction method implementation source code is mixed with a lot of scheduling and development environment related code. Here I have simplified the source code for the convenience of focusing on hook related principles:

  function dispatchAction<S.A> (fiber: Fiber,queue: UpdateQueue<S, A>,action: A,) {
    const update: Update<S, A> = {
      eventTime,
      lane,
      suspenseConfig,
      action,
      eagerReducer: null.eagerState: null.next: (null: any),
    };
  
    // Append the update to the end of the list.
    const pending = queue.pending;
    if (pending === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    queue.pending = update;
  
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
Copy the code

When the update phase is triggered, the following three things happen:

  1. Create an update object:
 const update: Update<S, A> = {
      eventTime,
      lane,
      suspenseConfig,
      action,
      eagerReducer: null.eagerState: null.next: (null: any),
    };
Copy the code

Here, we just need to focus on the Action and Next fields. As you can see from this, any arguments we pass to the Dispatch method are actions.

  1. Build updateQueue loop unidirectional linked list:
// Append the update to the end of the list.
    const pending = queue.pending;
    if (pending === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    queue.pending = update;
Copy the code

From the code above, you can see:

  • UpdateQueue is a cyclic one-way linked list;
  • The order in which the linked list elements are inserted is the same as the order in which the Dispatch method is called. That is, the last update object generated is at the end of the chain.
  • Queue.pending The pointer always points to the tail element.
  1. Actually trigger the update

In both the old class Component era and the present function Component era, when we call the corresponding setState() method or dispatch() method, the essence is to ask react to update the current component. Why do you say that? There’s a long way to go between a react update request and an actual DOM update. React’s batch update policy is now a new scheduling layer.

However, the idea with function Component in mind is that if React decides to update the current component, its call stack must go to a function called renderWithHooks. It’s inside this function that React calls our function Component (again, function Component is a function). If you call the function Component, you must call the hook. This means that the hook will enter the update phase.

So what happens to the hook in the update phase? Now, let’s see.

The update phase

Hook: updateReducer() ¶ hook: updateReducer() Because the updateReducer method implementation contains a lot of scheduling code, the following is a simplified version:

function updateReducer(reducer,initialArg,init) {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // Get the last element of the updated list
  const last = queue.pending;

  // Get the update object that was inserted first. Remember, this is a circular list: the next of the last one points to the first elementfirst = last ! = =null ? last.next : null;

  if(first ! = =null) {
    let newState = hook.baseState;
    let update = first;
    do {
      // Perform each update to update the status
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while(update ! = =null&& update ! == first); hook.memoizedState = hook.baseState = newState; }const dispatch = queue.dispatch;
  // Returns the latest status and the method to modify the status
  return [hook.memoizedState, dispatch];
}
Copy the code

In the update stage of Hook, the following two things mainly happened:

  1. Walk through the old hook chain, generate new hook objects by making a shallow copy of each hook object, and build new hook chains in turn.
  2. Iterate over the single linked list of update objects on each hook object, calculate the latest value, update it to the hook object, and return it to the developer.

The updateReducer() method is called to get a hook object. It’s in the updateWorkInProgressHook() method that implements the first thing we said. UpdateWorkInProgressHook ()

function updateWorkInProgressHook() :Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base. When we reach the end of the base list, we must switch to
  // the dispatcher used for mounts.
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if(current ! = =null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null; }}else{ nextCurrentHook = currentHook.next; } invariant( nextCurrentHook ! = =null.'Rendered more hooks than during the previous render.',); currentHook = nextCurrentHook;const newHook: Hook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    baseQueue: currentHook.baseQueue,
    queue: currentHook.queue,
    next: null};if (workInProgressHook === null) {
    // This is the first hook in the list.
    currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
  } else {
    // Append to the end of the list.
    workInProgressHook = workInProgressHook.next = newHook;
  }

  return workInProgressHook;
}
Copy the code

When talking about currentlyRenderingFiber said above, the current has been shown on the screen of the component of the fiber node is stored in the currentlyRenderingFiber alternate field. So, the old head of the hook chain pointer is currentlyRenderingFiber. Alternate. MemoizedState. The nextCurrentHook variable points to the specimen that is currently being copied, and the currentHook variable points to the specimen that has already been copied on the current old hook chain. Combined with these three semantics, this code is easy to understand:

let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if(current ! = =null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null; }}else {
    nextCurrentHook = currentHook.next;
  }
Copy the code

If the current is the first hook call, then the copied specimen object is the first element of the old hook chain. Otherwise, the copied specimen is the next in line to the one already copied.

The following code is a shallow copy of the hook object:

  currentHook = nextCurrentHook;

  const newHook: Hook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    baseQueue: currentHook.baseQueue,
    queue: currentHook.queue,
    next: null};Copy the code

From the shallow copy above, we can assume that the mount and update phases of hooks share the same queue.

Further down, even though the construction of the new linked list is almost the same as the construction of the hook linked list in the mount stage, it will not be described here:

   if (workInProgressHook === null) {
    // This is the first hook in the list.
    currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
  } else {
    // Append to the end of the list.
    workInProgressHook = workInProgressHook.next = newHook;
  }
Copy the code

We’ve done the first thing by parsing the updateWorkInProgressHook() source code. Next, we move on to the second thing – iterate through the single linked list of update objects on each hook, calculate the latest value, update it to the hook object, and return it to the developer. The updateReducer() method is a simplified version of the updateReducer() method. Let’s scratch it out again and zoom in:

 const queue = hook.queue;

  // Get the end of the queu loop
  const last = queue.pending;

  // Get the update object that was inserted first. Remember, this is a circular list: the next pointer to the last element points to the first elementfirst = last ! = =null ? last.next : null;

  if(first ! = =null) {
    let newState = hook.baseState;
    let update = first;
    do {
      // Perform each update to update the status
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while(update ! = =null&& update ! == first); hook.memoizedState = hook.baseState = newState; }const dispatch = queue.dispatch;
  // Returns the latest status and the method to modify the status
  return [hook.memoizedState, dispatch];
Copy the code

First, get the first element of the queue loop;

Second, start with the reducer(newState, action) and take actions from the list elements (update object). At the end of the loop, which is when the final value is calculated;

Finally, the new value is updated to the hook object and returned to the user.

From the second event, we can understand why the initialValue we passed was ignored when the hook was called in the update phase, and how the latest hook value was updated. The latest value is mounted on the hook object, which in turn is mounted on the Fiber node. When Component enters the Commit phase, the latest values are flushed to the screen. The hook thus completes the current update phase.

Why is the order of hooks so important?

In Rules of Hooks, there are two commandments for using Hooks:

  • Only Call Hooks at the Top Level
  • Only Call Hooks from React Functions(component)

If we just memorize things by rote, without exploring why and why, our memories won’t be strong. Now that we’re at the source code level, let’s explore the rationale behind this commandment.

First, let’s take a look at what these two commandments are really about. For the first one, the documentation clearly states that the purpose of calling a hook at the top of a function scope, rather than inside a loop, condition, or nested function, is one:

By following this rule, you ensure that Hooks are called in the same order each time a component renders.

Yes, the purpose is to ensure the mount phase, the first update phase, and the second update phase…… Between the NTH update phase, all hooks are called in the same order. We’ll explain why later.

The React Hook can only be called in the React function Component. This commandment is obvious. React Hook is a class component feature that must be associated with rendering components. Common javascript functions are not associated with rendering the React interface. The precepts more accurately read: Make sure react Hook is called under the react Function Component’s scope. That is, you can wrap react Hooks like a Russian nesting doll and follow the first precepts (custom hooks), but make sure the outermost function is called inside React Function Componnet.

What is the basis of this statement? That’s because the hooks are mounted to the Dispatcher and the dispatcher is injected at run time. The dispatcher injection occurs when the renderWithHook() method is called, as mentioned in the facts section above. The call stack from renderWithHook to hook looks like this:

RenderWithHook () -> Component () -> hook() -> resolveDispatcher()Copy the code

A quick look at the source code for the resolveDispatcher method will provide the basis for what we are looking for:

 function resolveDispatcher() {
    var dispatcher = ReactCurrentDispatcher.current;

    if(! (dispatcher ! = =null)) {{throw Error( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem."); }}return dispatcher;
  }
Copy the code

RenderWithHook () is not executed if you don’t call a hook in the React Function Component. RenderWithHook () is not executed. The dispatcher was not injected, you call hook, and the dispatcher is null, so an error will be reported.

So that’s the analysis of the evidence behind the second commandment. As for the first commandment, it goes on and on about calling hooks in the same order. To understand this reason, first we need to understand what is [hook call order]?

What is the call order of hooks?

The answer is: “The order in which hooks are called in the [mount stage] is the order in which hooks are called.” In other words, we judge whether the sequence of hook calls is consistent when a component render is performed, referring to the sequence of hook calls determined in the mount stage. Here’s an example:

/ / the mount stage
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)

// The first update phase
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)

// The second update phase
const [age,setAge] = useState(28)
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
Copy the code

UseState (0) -> useState(‘ Sam ‘) -> useState(28) -> useState(28)

In short, the order of hook calls is the order established in the mount phase.

Conclusions about the order of hook calls

First, without further ado, let’s draw two conclusions about the order of hook calls:

  1. The number of hook calls should be consistent. More, less, will report an error.
  2. It is best to keep the hook locations consistent.

In fact, after thinking, students will know that the call sequence can be split into two aspects: the number of hooks should be the same, and the location of hook calls should be the same (that is, the same hook should be called in the same order).

It is fine to call hooks in the same order as the official documentation suggests. It does this to ensure that we don’t make mistakes and that we don’t do meaningless things (changing the location of the hook call doesn’t make much sense).

However, from a source point of view, the inconsistent location of a hook call does not necessarily cause a program to fail. If you know how to handle the reference returned by a hook call, your program will still work. The reason I’m talking about this is because I want to go beyond the official documented-only fetish and realize that the source code is the only measure of what is right and wrong.

So, first of all, let’s see why the first one works. Let’s first look at the case where a hook is called many times. Suppose we have code like this:

/ / the mount stage
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)

// The first update phase
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)
const [sex,setSex] = useState('male')
Copy the code

After the mount phase, we get a hook chain like this:

=============           =============             =============
| count Hook |  ---->   | name Hook  |  ---->     | age Hook  | -----> null= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =Copy the code

As mentioned above, one of the main tasks of the Update phase is to iterate over old hook chains to create new ones. In updateWorkInProgressHook, the action before you copy a hook object is to calculate the value of the current hook object, which is the variable nextCurrentHook:

  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if(current ! = =null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null; }}else {
    nextCurrentHook = currentHook.next;
  }
Copy the code

Let’s say that now we get to the fourth hook call in the update phase, the code will execute to nextCurrentHook = currenthook. next; . CurrentHook is the object that was copied successfully last time (the third time) and is stored on an old chain. CurrentHook: null currentHook: null currentHook: null currentHook: null currentHook: null That is, the current hook object (nextCurrentHook) is null. Our assertion failed and the program reported an error:

  // If our assertion fails, an error is throwninvariant( nextCurrentHook ! = =null.'Rendered more hooks than during the previous render.',);Copy the code

This is the case where a hook is called too many times. Let’s look at the case where the number of hook calls is low. Let’s focus directly on these lines in the renderWithHooks method:

export function renderWithHooks<Props.SecondArg> (
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
) :any {
  / /...
  let children = Component(props, secondArg);
  / /...
  constdidRenderTooFewHooks = currentHook ! = =null&& currentHook.next ! = =null;
  / /...invariant( ! didRenderTooFewHooks,'Rendered fewer hooks than expected. This may be caused by an accidental ' +
      'early return statement.',);return children;
}
Copy the code

The Component here is the function Component of our hook call. If all hooks are called and the next pointer to currentHook points to something other than null, then the next pointer moves less during the update phase. If the hook is called the same number of times, then currentHook is equal to the last element of the old hook chain, and our assertion does not fail.

From the above source analysis, we can know that the number of hook calls can not be more, also can not be less. React will generate an error, and the program will stop running.

Finally, we come to conclusion two.

To prove my point, let’s just run the following example:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>react hook</title>
</head>
<body>
    <div id="root"></div>
    <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    <script></script>
    <script>
        window.onload = function(){
            let _count = 0;
            const {
                useState,
                useEffect,
                createElement
            } = React

            const root  = document.getElementById('root')
            let count,setState
            let name,setName

            function Counter(){
               
                if(_count === 0) {/ / the mount stage
                    const arr1= useState(0)
                    count = arr1[0]
                    setState = arr1[1]

                    const arr2 = useState('sam')
                    name = arr2[0]
                    setName = arr2[1]}else if(_count >= 1) {/ / update phase
                    const arr1 = useState('little sam')
                    count = arr1[0]
                    setState  = arr1[1]
                     
                    const arr2 = useState(0)
                    name = arr2[0]
                    setName = arr2[1]
                }


                _count += 1
                return createElement('button', {onClick:() = > {
                        setState(count= > count + 1)
                    }
                },count)
            }
            ReactDOM.render(createElement(Counter),root)

        }
    </script>
</body>
</html>
Copy the code

Run the above example directly, and the application will work and the interface will appear correctly. This proves that my conclusion is correct. So why is that? That’s because in the UPDATE phase, the name of the hook is not important in order to properly hold the reference returned by the hook call, but its location. In the update phase, although the position of the hook call is changed, but we know that the first position of the hook object is still pointing to the mount phase of the count hook object, so I can still use the corresponding variable to continue, so that the subsequent render will not error.

The example above is only to support my second conclusion. In real development, we don’t do that. Or, at the moment, I don’t have a development scenario where I have to do this.

That’s why the sequence of react Hook calls is so important from a source perspective.

Other hook

The above two hooks useState and useReducer are only taken as examples to explain the React Hook principle. There are still many hooks not involved, such as useEffect, which is very important. But, if you go deep into the hook of the source code (the react/packages/react – the reconciler/SRC/ReactFiberHooks. New. Js) to see, in almost all of the following in common:

  • Both have mount and Update phases;
  • In the mount phase, the mountWorkInProgressHook() is called to generate hook objects. During the update phase, updateWorkInProgressHook() is called to copy and generate a new hook object. This means that at the same stage, no matter what type of hook you are, everyone is in the same hook chain.
  • Each hook corresponds to a hook object, and different types of hooks differ mainly in the value they mount on the hook.memoizedState field. For example, useState and useReducer mount State (the data structure of state is up to us), useEffect mounts effect objects, useCallback mounts arrays of callback and dependent arrays, and so on.

Among other hooks, useEffect is so important and relatively different that it is undoubtedly the most worthy of our in-depth exploration. If you have time, the next article might explore how it works.

conclusion

The resources

  • Deep dive: How do React hooks really work?
  • Under the hood of React’s hooks system
  • React hooks: not magic, just arrays
  • Making Sense of React Hooks
  • React Hooks