The introduction

React introduced the concept of hooks in V16.8. It’s also a major innovation of React.

Use hooks to extract state logic from components so that it can be individually tested and reused.

Hooks reuse state logic without modifying the component structure.

Hook development is also officially recommended, but it is limited to the very top level.

Do not call hooks in loops, conditions, or nested functions.

Because every rendering of a Hook is called in the same order, the main reason is to keep the Hook state correct between multiple useState and useEffect calls.

This article analyzes the execution sequence of Hook from the source code.

The sample code

First post sample code, according to the example to do source analysis.

function App() {
  const [count, setCount] = useState(0);

  useEffect(() = > {
    console.log('naonao', count);
  });

  let handleCount = () = > {
    setCount(count + 1);
  };

  return (
    <div>
      <p onClick={handleCount}>{count}</p>
    </div>
  );
}

ReactDom.render(<App />.document.getElementById('root'));
Copy the code

It’s very simple. I won’t explain it.

First render

The first rendering goes into the updateFunctionComponent method in the beinWork phase.

The renderWithHooks method is called in the updateFunctionComponent method.

The renderWithHooks method actually calls the function component method, the App function in the example.

We posted renderWithHooks source code

/ / this code on the packages/react - the reconciler/SRC/ReactFiberHooks. Old. Js
export function renderWithHooks<Props.SecondArg> (
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
) :any {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber = workInProgress;

  if (__DEV__) {
    / /... Omit development environment logic
  }

  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;

  
  if (__DEV__) {
    / /... Omit development environment logic
  } else {
    // Reset a pointer object to which hooks are called to distinguish between the mount and update phases
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }
  // Call the function component
  let children = Component(props, secondArg);

  
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
     / /... Omit code and retrigger function component execution if an update is triggered during the render phase
    } while (didScheduleRenderPhaseUpdateDuringThisPass);
  }

  // Reset the pointer object for hooks
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  if (__DEV__) {
    / /... Omit development environment logic
  }

  // Check whether all the hooks are called
  constdidRenderTooFewHooks = currentHook ! = =null&& currentHook.next ! = =null;

  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);
  // Reset to null for re-generation the next time hooks are called
  currentHook = null;
  workInProgressHook = null;

  if (__DEV__) {
    / /... Omit development environment logic
  }

  didScheduleRenderPhaseUpdate = false;
  // If the hook is not called completely, an error is throwninvariant( ! didRenderTooFewHooks,'Rendered fewer hooks than expected. This may be caused by an accidental ' +
      'early return statement.',);return children;
}
Copy the code

Take a look at some key logic:

  1. let children = Component(props, secondArg); This step is to execute the App method, which calls useState and useEffect within the method.
  2. const didRenderTooFewHooks = currentHook ! == null && currentHook.next ! == null; , this step is to check whether all hooks are executed. If not all are executed, below invariant(! didRenderTooFewHooks,’Rendered fewer hooks than expected. This may be caused by an accidental ‘ +’early return statement.’,); Throw an error.
  3. currentHook = null; workInProgressHook = null; Reset the variable to NULL. CurrentHook and workInProgressHook are global variables.

In the packages/react – the reconciler/SRC/ReactFiberHooks. Old. Js line 152.

// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
Copy the code

The Hooks are stored as a linked list on fiber’s memoizedState property. CurrentHook belongs to the current Fiber list. WorkInProgress shook is a new linked list that needs to be added to workInProgress.

Ok, so let’s look at the execution of our function App.

useState

The App function starts with useState.

When first rendered, useState points to mountState.

/ / this code on the packages/react - the reconciler/SRC/ReactFiberHooks. Old. Js
function mountState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  // call build list
  const hook = mountWorkInProgressHook();
  If it is a function, execute it
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  // Assign the base state of the hook
  hook.memoizedState = hook.baseState = initialState;
  // Build a queue of hooks
  const queue = (hook.queue = {
    pending: null.dispatch: null.lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  // Build the trigger function
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}
Copy the code

Focus on the mountWorkInProgressHook function, which builds a list of hooks.

function mountWorkInProgressHook() :Hook {
  // hook list object
  const hook: Hook = {
    memoizedState: null.baseState: null.baseQueue: null.queue: null.next: null};if (workInProgressHook === null) {
    // First hook
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Start from the second hook and hang at the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
Copy the code

CurrentlyRenderingFiber is the currently rendered fiber.

When the first hook, workInProgressHook is null then the current hook object assigned to give currentlyRenderingFiber workInProgressHook assignment at the same time. MemoizedState.

When the second hook is attached to the next property of the first hook, namely workInProgressHook, Because workInProgressHook and currentlyRenderingFiber memoizedState address pointing to the same, so also modify currentlyRenderingFiber. MemoizedState linked list.

The third, the fourth the same logic eventually builds into a single necklace list.

The function App then continues with useEffect.

useEffect

For the first rendering, useEffect points to mountEffect.

MountEffect calls mountEffectImpl directly and returns its execution result.

function mountEffectImpl(fiberFlags, hookFlags, create, deps) :void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}
Copy the code

The mountWorkInProgressHook method is called on the first line of the function.

So our sample code builds a linked list with two nodesHook

At the end of the App function, return to renderWithHooks.

Const didRenderTooFewHooks = currentHook! == null && currentHook.next ! == null; CurrentHook is null because it does not modify the assignment process.

CurrentHook = null; workInProgressHook = null; Then the normal process is to reconcile the child node, build the fiber of the child node and the corresponding real DOM, complete the construction, and finally render to the page.

We click on the P element to trigger a change in state. This enters the update render logic.

The setCount method executes the dispatchAction method and then enters the beginWork logic again.

And you also end up calling the renderWithHook method. But the ReactCurrentDispatcher. Current will point HooksDispatcherOnUpdate.

Let children = Component(props, secondArg); I’m going to execute the function App.

The App function is executed, starting with the useState method.

UseState in this case points to Update Estate.

updateState

UpdateState executes and returns the updateReducer result.

/ / this function complete code on the packages/react - the reconciler/SRC/ReactFiberHooks. Old. Js
function updateReducer<S.I.A> (reducer: (S, A) => S, initialArg: I, init? : I => S,) :S.Dispatch<A>] {
  // Update the hook list
  const hook = updateWorkInProgressHook();
  / /... Omit other logic

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}
Copy the code

Let’s leave out the other logic and focus on the updateWorkInProgressHook method at the beginning.

This differs from the logic of mountState.

UpdateWorkInProgressHook is where the currentHook is assigned and the workInProgressHook is updated.

So the update work in progress shook method we’ll talk about in a second, but let’s look at the useEffect because it also calls the update work in progress shook.

updateEffect

UseEffect points to updateEffect.

The updateEffect method executes and returns the result of updateEffectImpl.

/ / this function complete code on the packages/react - the reconciler/SRC/ReactFiberHooks. Old. Js
function updateEffectImpl(fiberFlags, hookFlags, create, deps) :void {
  const hook = updateWorkInProgressHook();
  / /... Omit other logic
}
Copy the code

The first line of this function is to call the updateWorkInProgressHook method.

Ok, so let’s focus on the Update Work in Progress Shook.

updateWorkInProgressHook

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

  let nextWorkInProgressHook: null | Hook;
  // The second if
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  // The third if
  if(nextWorkInProgressHook ! = =null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    Clone from current hookinvariant( 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};// The fourth if
    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else{ workInProgressHook = workInProgressHook.next = newHook; }}return workInProgressHook;
}
Copy the code

I divided the logic into four if logic and numbered it in the code.

Because useState and useEffect are called in App, they are executed twice.

We analyze the logic of the first execution and the logic of the second execution.

The logic of the first walk

CurrentHook is null and the first if enters the if branch.

CurrentlyRenderingFiber is the currentlyRenderingFiber (App), take its alternate.

becausealternateDon’t fornullWill take itmemoizedStateFetch the assignment tonextCurrentHook. Remember, the first render of our application is going to takehookList hanging onmemoizedStateOn. suchnextCurrentHookThat points to having two of themhookThe linked list.

The first if logic completes. And then the second if.

At this timeworkInProgressHookIs alsonull, will go to number twoififBranch. whilecurrentlyRenderingFiber.memoizedStatenullAnd assign a value tonextWorkInProgressHook

Then the third if logic.

NextWorkInProgressHook is null, which enters the else branch of the third if logic.

Assign the nextCurrentHook to currentHook first, so that currentHook points to a list with two nodes.

Const newHook clones the property of the first node in the list.

WorkInProgressHook is still null into the if branch of the fourth if logic.

thenewHookAssigned toworkInProgressHookcurrentlyRenderingFiber.memoizedState. suchworkInProgressHookI have a value and I have a new valuefiberattributememoizedStateIt also has values, but it’s just a linked list with one node,nextnull.

This is how the updateWorkInProgressHook method is called during useState execution.

CurrentHook points to a list with two nodes, workInProgressHook points to a list with one node, and the property value is clone from the first node of currentHook.

The logic of going the second time

The second call to updateWorkInProgressHook is triggered by useEffect.

CurrentHook is not null, the first if else branch.

NextCurrentHook points to currentHook. Next, which is actually useEffect Hook.

Then enter the second if logic, where the workInProgressHook is not null, and enter the else branch.

NextWorkInProgressHook is null because next of workInProgressHook is null.

The third if logic, nextWorkInProgressHook, is null, so it enters the else branch.

After assigning the nextCurrentHook to currentHook, clone the new newHook again.

If (workInProgreshook. next, workInProgreshook. next, workInProgreshook. next, workInProgreshook. next) Thus build the has two nodes linked list, currentlyRenderingFiber. MemoizedState also have the two linked list of nodes.

For App functions, useState and useEffect are completed, so return is completed. Then go back to the renderWithHooks method.

Const didRenderTooFewHooks = currentHook! == null && currentHook.next ! == null; CurrentHook is not null but its next is null, so didRenderTooFewHooks is false.

In this way, on the following invariant(! didRenderTooFewHooks,’Rendered fewer hooks than expected. This may be caused by an accidental ‘ +’early return statement.’,); Will not throw an error.

A hook is put in a conditional statement

Let’s modify the useEffect execution conditions

function App() {
  const [count, setCount] = useState(0);
  
++ if (count === 0) {
    useEffect(() => {
      console.log('naonao', count);
    });
+ +}

  let handleCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p onClick={handleCount}>{count}</p>
    </div>
  );
}

ReactDom.render(<App />, document.getElementById('root'));
Copy the code

So there’s no logic in the Update Work in Progress method, currentHook is a two-node linked list, Const didRenderTooFewHooks = currentHook! In renderWithHooks method == null && currentHook.next ! == null; DidRenderTooFewHooks is true, so it throws an error.

conclusion

Hooks end up building a single linked list, and each update execution is called in the same order. If a Hook is executed in a loop, condition, or nested function, it will break its order and cause problems.

Use the eslint-plugin-react-hooks plugin to check compliance with the rules at compile time.