Two rules

The React Hooks documentation describes the following rules for using React Hooks.

Use hooks only at the top level

Never call hooks in loops, conditions, or nested functions. Make sure you always call them at the top of your React function. By following this rule, you can ensure that hooks are called in the same order every time you render. This allows React to keep the hook state correct between multiple useState and useEffect calls.

Only the React function calls the Hook

Never call a Hook in a normal JavaScript function. You can:

  • ✅ calls the Hook in the React function component
  • ✅ calls other hooks in custom hooks

We need to follow these two rules when using hooks, so why these two restrictions? We can look for the answer in the source code.

Hooks basis

Like the class component’s setState method, the hooks function we use does not implement code directly, but instead calls the corresponding function in an object called dispatcher. The source address

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  return dispatcher;
}

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

export function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null.) :void {
  const dispatcher = resolveDispatcher();
  returndispatcher.useEffect(create, deps); }...Copy the code

The function component is executed inside the renderWithHooks function.

// Remove code irrelevant to this article
export function renderWithHooks<Props.SecondArg> (
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
) :any {
  // workInProgress represents the Fiber corresponding to the function component in this update
  currentlyRenderingFiber = workInProgress;
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  
  // current represents the Fiber corresponding to the function component in the last update
  // current === NULL Indicates that the component is in the mount phase; otherwise, the component is in the update phase
  / / different stages ReactCurrentDispatcher. Current is equal to the different objects
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;

  // Execute Component, which is our function Component
  let children = Component(props, secondArg);
  
  / / function components performed, will ReactCurrentDispatcher. Current for ContextOnlyDispatcher
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  currentlyRenderingFiber = null;
  currentHook = null;
  workInProgressHook = null;

  return children;
}

// Mount the associated dispatcher
const HooksDispatcherOnMount: Dispatcher = {
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  ...
};

// Update the relevant dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  ...
};

// Execute dispatcher that throws errors
export const ContextOnlyDispatcher: Dispatcher = {
  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  useMemo: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  ...
};
Copy the code

Why only call a Hook in the React function

UseState, for example, can call ReactCurrentDispatcher useState internal current. UseState, Because only in front of the function component implementation will ReactCurrentDispatcher. The current Settings for HooksDispatcherOnMount or HooksDispatcherOnUpdate, When the function component after the execution ReactCurrentDispatcher. Current is set to ContextOnlyDispatcher immediately, so in the React when using useState outside a function, UseState internal calls ContextOnlyDispatcher useState, this function will be an error. Same with other hooks.

Why only use hooks at the top level

Each call to hooks generates a hook object, which is structured as follows

export type Hook = {|
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null.queue: UpdateQueue<any, any> | null.next: Hook | null|};Copy the code

Focus on the memoizedState and next properties, the memoizedState of the hook objects of different hooks holds different objects

  • useState:hook.memoizedState = state;
  • useEffecthook.memoizedState = effect
  • useMemo:hook.memoizedState = [nextValue, nextDeps];
  • useCallback:hook.memoizedState = [callback, nextDeps];
  • useRef:hook.memoizedState = ref;

This produces a list of hook objects linked by the next property. The link is made at mount time. The main function is the mountWorkInProgressHook.

function mountWorkInProgressHook() {
  const 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

When mounted, hooks call this function to generate a hook object and connect. If workInProgressHook = = = null, that is the first of the hook chain, will be assigned to currentlyRenderingFiber. MemoizedState and update workInProgressHook, This indicates that the memoizedState of the function component Fiber holds a reference to the first hook. When workInProgressHook! == null, then the newly generated hook is assigned to the next attribute of the previous hook and the workInProgressHook is updated.

When updating, hooks call the updateWorkInProgressHook function to generate a new hook object and connect it to form a hook chain for the next update.

function updateWorkInProgressHook() :Hook {
  let nextCurrentHook: null | Hook;
 
  if (currentHook === null) { // currentHook === null indicates that the currentHook is the first one
    // Current is the Fiber corresponding to the component updated last time
    const current = currentlyRenderingFiber.alternate;
    if(current ! = =null) {
      // memoizedState saves the first hook of the hook chain
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null; }}else {
    Currenthook. next, the next hook of the hook chain from the last update
    nextCurrentHook = currentHook.next;
  }
  // At this point nextCurrentHook represents the hook object of this hooks from the last update
  
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) { //workInProgressHook === null
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }
  // nextWorkInProgressHook is mostly null at this point, because renderWithHooks function will be used before executing the function component
  / / currentlyRenderingFiber memoizedState set to null, workInProgressHook. Next is null
  
  if(nextWorkInProgressHook ! = =null) { // Ignore this branch
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    // Most of the time, this branch is entered
    
    CurrentHook is the corresponding sequence of hook objects from the last update
    currentHook = nextCurrentHook;
    
    // Generate a new hook object by reusing the properties of the last updated hook object
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null};// Form a new hook chain, just like when mounting
    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 hooks are correctly used, there is no problem iterating through the hook chain and reusing the last updated hook, but if a certain hooks are called conditionally, then the situation is different. For a simple example, use hooks like this:

function FunComponent(props) {
    if (props.condition) {
        useEffect()
    }
    const [counter, setCounter] = useState(0)
    const memo = useMemo()
}
Copy the code

Condition === = true; useEffect hook => useState hook =>useMemo hook

On the second update, props. Condition === false, we go through the update process.

UseState is executed first. It calls updateWorkInProgressHook, currentHook is the first hook of the last update hook chain. Then reuse the hook property to get a new hook and return it to useState for use. UseState then return the memoizedState of the hook object, but at this time the memoizedState of the hook object is not a number but an effect object. Counter has changed from a number to an object, rendering or counting with counter will definitely give you an error.

In the same way, useMemo calls the updateWorkInProgressHook function and gets useState Hook. This error is more serious because useMemo uses the memoizedState of the hook object as an array. Now the Hook. memoizedState is a number. Take a look at the source code of useMemo when it is updated.

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) {
    if(nextDeps ! = =null) {
      // Last dependency
      const prevDeps: Array<mixed> | null = prevState[1];
      // Dependencies do not change
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0]; }}}const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
Copy the code

conclusion

  1. The reason not to call hooks in normal JavaScript functions is that the hooks functions used in other functions are actually functions called throwInvalidHookError.

  2. The reason not to call hooks from a loop, condition, or nested function is that changing the order in which hooks are executed will cause the wrong Hook object to be used in hooks.