Understand useState from the source code

Pre-school thought questions:

  1. React Hooks Why must they be executed inside function components? How does React listen for React Hooks to execute externally and throw exceptions?
  2. How do React Hooks save state? Where is the saved information stored?
  3. React Hooks cannot be used in conditional statements

Pay attention to

The source code for this article is React 16.13.1

React Hooks relate to Fiber

Double cache Fiber tree

We know that Act16 has enabled a new architecture called Fiber. In React, at most two Fiber trees will exist at the same time. The current HTML page should have a Fiber tree called the current Fiber tree, and the workInProgress Fiber tree is the Fiber tree under construction. When the workInProgress Fiber tree is constructed and rendered, the current pointer will point to the workInProgress Fiber tree, and then the workInProgress Fiber tree will become the Current Fiber tree. This is the double-cached Fiber tree.

Fiber with Hooks

The fiber node saves the hooks information with fiber.memoizedState.

Hook object

Fibre. memoizedState records the hooks information of the current node, and hooks form a linked list using the hook. next pointer. * memoizedState * memoizedState * memoizedState * memoizedState * memoizedState * memoizedState

type Hook = {
  memoizedState: any./ / save the state
  baseState: any.baseQueue: Update<any.any> | null.queue: UpdateQueue<any.any> | null.next: Hook | null.// Point to the next hook to form a linked list
};

type UpdateQueue<S, A> = {
  pending: Update<S, A> | null.// Point to the update to be updated
  dispatch: (A= > mixed) | null.lastRenderedReducer: ((S, A) = > S) | null.lastRenderedState: S | null};type Update<S, A> = {
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
  action: A,
  eagerReducer: ((S, A) = > S) | null.eagerState: S | null.next: Update<S, A>, priority? : ReactPriorityLevel, };Copy the code

HooksDispatcherOnMount,HooksDispatcherOnUpdate

Also is to perform the React. UseState (), an initialization function components and update process, the code logic hooks is not the same, the main reason is that perform the React. UseState (), from ReactCurrentDispatcher. Current value, And that value will change. To execute react.usestate (), you actually execute the useState(initialState) function in reacthooks.js,

// React.usestate () is what this function does
  function useState(initialState) {
    var dispatcher = resolveDispatcher();
    return dispatcher.useState(initialState);
  }

/ * * get the current dispatcher, namely ReactCurrentDispatcher. The current value of * /
  function resolveDispatcher() {
    vardispatcher = ReactCurrentDispatcher.current; .return dispatcher;
  }
Copy the code

Is the React by changing ReactCurrentDispatcher. The value of the current under different states perform different logic Hooks, change the logic in renderWithHooks ().

The code is as follows :(reactfiberlinks.js)

renderWithHooks(){...// For the first time, the logic will be true and HooksDispatcherOnMount; When updated, HooksDispatcherOnUpdate is assigned
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;

    let children = Component(props, secondArg); // Execute the function component
 
  // After executing the component, ContextOnlyDispatcher is assigned, and an error is reported if useState() is also calledReactCurrentDispatcher.current = ContextOnlyDispatcher; . }Copy the code

There are three kinds of ReactCurrentDispatcher. The value of the current situation:

  1. ContextOnlyDispatcher: error pattern, which raises an exception whenever the developer calls hooks in this pattern.
  2. HooksDispatcherOnMountThe mount function component initializes the relationship between hooks and Fiber for the first time.
  3. HooksDispatcherOnUpdateFiber is now connected to the fiber bridge, which requires hooks to get/update the maintenance state.

When initialized, it is HooksDispatcherOnMount; When updated, it is HooksDispatcherOnUpdate; When the function component is executed, ContextOnlyDispatcher is assigned. If react.usestate () is executed, throwInvalidHookError will be generated and an error will be reported.

The values of HooksDispatcherOnMount, HooksDispatcherOnUpdate, and ContextOnlyDispatcher are as follows:

const HooksDispatcherOnMount = { /* Hooks */ used to initialize function components
    useState: mountState,   // execute at initialization time
    useEffect: mountEffect,
    ...
}
const  HooksDispatcherOnUpdate ={ /* Hooks */ for function component updates
   useState: updateState, // Execute at update time
   useEffect: updateEffect,
   ...
}
const ContextOnlyDispatcher = {  /* An error is reported because hooks under this object are called when hooks are not called internally. * /
   useEffect: throwInvalidHookError,
   useState: throwInvalidHookError,  // Throw an error. }Copy the code

The useState command is executed for the first time

When the component is first loaded, the fiber node is built from beginWork, which executes function components, then react.usestate () in function components, all the way to mountState. At first load, the important logic of useState is in mountState, creating hook objects and queue objects, initializing the state value, Finally return state and dispatchAction (binding the current Fiber node and queue to dispatchAction).

  function mountState(initialState) {

    var hook = mountWorkInProgressHook(); // Generate a Hook object. If it is the first Hook, it will be stored in the fiber. MemoizedState property

    if (typeof initialState === 'function') {
      initialState = initialState();
    }

    hook.memoizedState = hook.baseState = initialState; // Initialize state

    var queue = hook.queue = {  // Initialize the queue
      pending: null.dispatch: null.lastRenderedReducer: basicStateReducer,
      lastRenderedState: initialState
    };
    
    // Bind the current fiber node and queue to dispatchAction (for updates)
    var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
    
    return [hook.memoizedState, dispatch];
  }
Copy the code

The react. useState is initialized for the first time, and the following data structure is generated:

UseState update

The update is divided into two parts:

  1. dispatchAction
  2. Schedule updated

1, dispatchAction

From the initialization process of useState above, useState finally returns [hook.memoizedState, dispatchAction].

 const [num, setNum] = React.useState(100); // [hook.memoizedState, dispatch]
Copy the code

So when we update state, setNum() is actually dispatchAction (note that the dispatchAction returned is bound to the fiber node and queue).

Take a look at the logic of dispatchAction:

function dispatchAction(fiber: Fiber,queue: UpdateQueue, action) {...const update: Update> = {  // Create an update object. action,// That is, setNum() is passed to the update object
    eagerReducer: null.eagerState: null.next: (null: any),};Queue.pending points to the update list
  const pending = queue.pending;

  update.next = pending.next;
  pending.next = update;

  queue.pending = update;

  // Whether the fiber node being updated is currently fiber
  if(fiber === currentlyRenderingFiber || (alternate ! = =null && alternate === currentlyRenderingFiber)) {
     ... 
    Fiber is currently undergoing a harmonic rendering update, so no update is required and no new scheduling is initiated
  } else {
    if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
        var currentState = queue.lastRenderedState; // State before update
        var eagerState = lastRenderedReducer(currentState, action);  // Calculate the updated state (action will be executed)

        if (objectIs(eagerState, currentState)) { // If the state before and after the update is the same, the update will not be initiated
          return;
        }
    }
    scheduleWork(fiber, expirationTime); // scheduleWork is scheduleUpdateOnFiber, which initiates update scheduling
}
Copy the code

According to the above logic, when the state before and after the update is the same, the update will not be initiated. (If setNum() is a function, it will be executed regardless of whether the state is the same.)

To summarize: dispatchAction does two things: 1. Generates update lists. 2. Initiate update scheduling

After execution, the data structure in memory is as follows:

2. Perform scheduling updates

const scheduleWork = scheduleUpdateOnFiber
Copy the code

DispatchAction calls scheduleWork (scheduleUpdateOnFiber) to initiate update scheduling.

The workInProgress Fiber tree will be built, and the beginWork() function will be built, but this time it will go to the Case FunctionComponent branch, but renderWithHooks() will be executed, Same logic as initialization.

    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
Copy the code

Different is the judgment condition is false, because now is updated, so ReactCurrentDispatcher. Current = HooksDispatcherOnUpdate, It is perform useState take HooksDispatcherOnUpdate useState, logic is updateState, finally actual execution is updateReducer ().

  function updateState(initialState) {
   return updateReducer(basicStateReducer);
 }
Copy the code

UpdateReducer () is an important section of logic in the useState update phase. It executes the update list on the hook one by one and returns the final state. In this way, the function component can obtain the updated state.

function updateReducer(reducer, initialArg, init) {

 const hook = updateWorkInProgressHook(); // Get the basic information of the current hook based on the old hook.// Fetch the pending queue.do {   // Retrieve the update.action execution from the update list to get the final state
     const action = update.action;

     newState = reducer(newState, action);

     update = update.next;
   } while(update ! = =null&& update ! == first);// Save the current statehook.memoizedState = newState; hook.baseState = newBaseState; .const dispatch = queue.dispatch;
 return [hook.memoizedState, dispatch];
}
Copy the code

UpdateReducer () takes the update list generated in dispatchAction and executes it one by one in the do while {} loop to obtain the final state, which is the value obtained by useState in the function component.

Why can’t hooks be written in if conditions?

In updateReducer, the updateWorkInProgressHook is based on the hooks in the current fiber tree, which are reused to get the current newHook.

function updateWorkInProgressHook() {... nextCurrentHook = currentHook.next;// currentHook is a list of hooks in the Current Fiber tree. currentHook = nextCurrentHook;const newHook: Hook = {  // Build a workInProgressHook based on current hook
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null}; workInProgressHook = workInProgressHook.next = newHook;return workInProgressHook;
}
Copy the code

NextCurrentHook = currenthook. next Every time useState is executed, a hook. Next is retrieved from the hooks list in the current tree. If you write an if condition, you’ll get a mismatch when you use next.

Conclusion:

  1. Hooks form linked lists,fiber.memoizedStateWill point to the list (for fiber, the function component, the class component)fiber.memoizedStateSave something else)
  2. setNum()The actual implementation isdispatchAction, produces the Update linked list,hook.queue.pendingPoint to the UPDATE list
  3. setNum()A new update schedule is initiated
  4. useStateWhen updating, the update list is pulled out and executed in sequence to get the final state.

When you use custom hooks in a function component, the hooks from the custom hooks are also on the hook list.


References:

React: github.com/Terry-Su/de…

React: react.iamkasong.com/#%E5%AF%BC%…

Nuggets of Gold: The React Advanced Practice Guide (paid)