In the process of using the hooks API, I have often encountered some problems, sometimes with the understanding of the API, sometimes with the confusion of why it is so, so I spent a few days looking through the source code, and here is a summary, which is also some personal experience to share. The following is the text of this article, which introduces the three key points of the hooks implementation and the common API source parsing based on the three key points.

Three main points:

  1. Hooks state updates depend on closures.
  2. In addition, because the function component will re-execute the function when it is updated, we need to record the hooks we created during the first rendering. React uses the linked list approach.
  3. For each hooks, we need a linked list of the state links that we update for that hooks (insert new updates, order of previous updates)

Since useState

Closure use:

We start from the basic useState source code, useState is essentially a simplified version of useReducer, useReducer and Redux are basically the same way to use, here will not repeat, Interested students can see the previous articles (redux | redux – thunk | react – story from foundation to use the source code analysis), the react provides a default reducer for the update, useState is divided into two states: Mount and update, so there are two corresponding methods:

  1. MountState to determine whether the initial value is a function, mount a hook on fiber node corresponding to workInProgress, whose baseState and memoizedState are both initial values, pass in the default reducer, return dispatch, Finally, hook. MemorizedState and dispatch are returned as arrays
  2. UpdateState simply calls the default reducer and replaces state
// react-reconciler/src/ReactFiberHooks.js
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;
}
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];
}
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}
Copy the code

The chain of hooks

We found that the mountWorkInProgressHooks function is executed when each hooks are mounted. Here we look at the code to see what this function does. It creates a new hook, determines if there is a first hook, and then lists the hooks. Return the newly created hook, the last item in the linked list, to form the hooks as shown below!

// react-reconciler/src/ReactFiberHooks.js
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

Dispatch the update of the chain

Each time we execute the dispatchAction method, we create an Update object that holds this update and add it to the update queue.

React uses a circular linked list data structure. When inserting a second node, the next of the second node points to last’s Next, and last’s next points to the new node. This results in a structure like the one shown below, which clears the chain after this update and then changes the current value to the latest value

The update process starts at the next node (the first update) of the node to which the current hook points, and moves forward until the list is completed

// react-reconciler/src/ReactFiberHooks.js function dispatchAction(fiber,queue,action,) { const update = { action, next: null, }; // Add update object to loop list const last = queue.last; If (last === null) {// The list is empty, make the current update the first, and keep the loop update.next = update; } else { const first = last.next; if (first ! Update. Next = first; // Insert new update object after latest update object. } last.next = update; } // Queue. Last = update; // scheduleWork(); }Copy the code

useEffect 

The use of useEffect is also divided into mount and update. In the mount stage, effect is mounted to two places: hooks and pushEffect. UseEffect is mounted to updateQueue. The update ue function is then executed after the refresh is complete. In the update phase, this is basically the same, except that a dePS judgment is added. If the DEPS does not change, the tag does not need to be updated, and the function is not executed during the update ue

// react-reconciler/src/ReactFiberHooks.js
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect | PassiveStaticEffect,
    HookPassive,
    create,
    deps,
  );
}
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.effectTag |= fiberEffectTag;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag,
    create,
    undefined,
    nextDeps,
  );
}
function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}
Copy the code

UseMemo and useCallback

The mount procedure retrieves and stores the initial value, while the update procedure executes a new function to retrieve the new value or replace the value with the new value based on its shallow compare. In essence, they take advantage of context switching. A function or variable that existed in the previous context and, if dePS changes, uses or executes the function in the current context.

The value of the useMemo is cached at mount time. If the DEPS does not change, the function is not updated and the value is not updated. UseCallback is the same with useCallback, but it is a little bit difficult to understand. I have not understood why the variables in the function are not updated when I use it. Memorized functions only hold the values of the variables in the corresponding state. When function is re-executed, references to variables will not change. Context is switched after the DEPS update. Here’s a little example to help you understand

/ / useMemo related function mountMemo < T > (nextCreate: () = > T, deps: Array < mixed > | void | null,) : T { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; } function updateMemo<T>( nextCreate: () => T, deps: Array<mixed> | void | null, ): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState ! == null) { // Assume these are defined. If they're not, areHookInputsEqual will warn. if (nextDeps ! == null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; } / / useCallback related function mountCallback < T > (deps callback: T: Array < mixed > | void | null) : T { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; hook.memoizedState = [callback, nextDeps]; return callback; } function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState ! == null) { if (nextDeps ! == null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } hook.memoizedState = [callback, nextDeps]; return callback; }Copy the code

example

let obj = {} 
function area() {
  let b = 666
  const test = () => {
    console.log(b)
  }
  obj.test = test
}
area()
function area2() {
  let b = 999
  obj.test()
}
area2()
Copy the code

useRef

As you can see from the example above, the function Component will re-execute the function to switch to the new context when updating, so if you want to hold the original value, you need to hold the value in Fiber’s memorizeState and retrieve it from Fiber when using it. So useRef API, the source code is very simple, I don’t want to repeat here.

function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  if (__DEV__) {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}

function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}
Copy the code

Overall structure drawing

data

  1. The how and why on React’s usage of linked list in Fiber to walk the component’s tree
  2. Explore the React kernel: Drill down into the Fiber architecture and coordination algorithm ✨✨✨✨ port
  3. ✨✨✨✨
  4. What exactly are we using when we are using Hooks? ✨ ✨ ✨