React Source Code

    8. Functional components-construction details and updates (Part 2)

    Published
    November 20, 2022
    Reading Time
    3 min read
    Author
    Felix
    Access
    Public

    The introduction of React Hooks has completely changed the way React components are written, enabling functional components to manage state and side effects. This article will delve into the internal implementation of React Hooks, especially the commonly used Hooks useEffect, useRef, useMemo and useCallback.

    Internal mechanism of Effect Hook

    Effect Hook (mainly useEffect and useLayoutEffect) in React is an important tool for handling component side effects. Their implementation involves multiple stages of React's rendering and submission process. Let’s take a closer look at how they work.

    The difference between useEffect and useLayoutEffect

    Although the APIs of useEffect and useLayoutEffect look similar, there are important differences in their execution timing:

    • useEffect: executed asynchronously after rendering is completed

    • useLayoutEffect: executed synchronously after DOM changes

    This difference is reflected in their internal implementation:

    function mountEffect(create, deps) {
      return mountEffectImpl(UpdateEffect | PassiveEffect, HookPassive, create, deps)
    }
    
    function mountLayoutEffect(create, deps) {
      return mountEffectImpl(UpdateEffect, HookLayout, create, deps)
    }
    

    The key difference is the flags passed in: useEffect uses the PassiveEffect flag, while useLayoutEffect does not. This determines how they are processed during the commit phase.

    Effect creation process

    When the component is first rendered, Effect is created through the mountEffectImpl function:

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

    This function performs the following key tasks:

    1. Create a new hook and add it to the fiber's hook list

    2. Set the flags of the fiber, which determines how to handle the fiber during the commit phase.

    3. Call pushEffect to create an effect object and add it to the effect list

    Effect execution process

    The execution of Effect is divided into three main stages, corresponding to the three sub-stages of the commit stage:

    1. Before mutation (commitBeforeMutationEffects)

    2. Mutation (commitMutationEffects)

    3. Layout (commitLayoutEffects)

    Before Mutation stage

    At this stage, React prepares for the execution of useEffect:

    function commitBeforeMutationEffects() {
      while (nextEffect !== null) {
        const flags = nextEffect.flags
        if ((flags & Passive) !== NoFlags) {
          if (!rootDoesHavePassiveEffects) {
            rootDoesHavePassiveEffects = true
            scheduleCallback(NormalSchedulerPriority, () => {
              flushPassiveEffects()
              return null
            })
          }
        }
        nextEffect = nextEffect.nextEffect
      }
    }
    

    Here, React schedules an asynchronous task to execute useEffect. That's why useEffect executes asynchronously after rendering.

    Mutation stage

    At this stage, React handles useLayoutEffect’s cleanup function:

    function commitMutationEffects(root, renderPriorityLevel) {
      while (nextEffect !== null) {
        const flags = nextEffect.flags
        if (flags & Update) {
          const current = nextEffect.alternate
          commitWork(current, nextEffect)
        }
        nextEffect = nextEffect.nextEffect
      }
    }
    

    This process is synchronous and occurs before the DOM is updated, ensuring that old side effects are cleaned up before new ones are executed.

    Why useLayoutEffect performs cleanup function here?

    • Make sure to clean up old side effects before updating the DOM to prevent potential memory leaks.

    • Prepare a clean environment for new side effects.

    Layout stage

    In the Layout phase, React executes the callback function of useLayoutEffect:

    function commitLayoutEffects(root, committedLanes) {
      while (nextEffect !== null) {
        const flags = nextEffect.flags
        if (flags & (Update | Callback)) {
          const current = nextEffect.alternate
          commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes)
        }
        if (flags & Ref) {
          commitAttachRef(nextEffect)
        }
        nextEffect = nextEffect.nextEffect
      }
    }
    

    useLayoutEffect's callback is executed synchronously here, while useEffect's callback is still waiting for asynchronous execution.

    Asynchronous execution of useEffect

    The actual execution of useEffect occurs in the previously scheduled asynchronous task. When the browser has idle time, the previously scheduled asynchronous task (flushPassiveEffects) will be executed. This function will loop through all pending effects and execute them:

    function flushPassiveEffects() {
      if (rootWithPendingPassiveEffects !== null) {
        const root = rootWithPendingPassiveEffects
        // First execute all cleanup functions
        flushPassiveEffectsImpl()
        // Then execute the new effect
        flushPassiveEffectsImpl()
      }
    }
    

    This function first executes the cleanup functions of all effects to be cleaned, and then executes all new effect callbacks.

    2. Implementation principle of Ref Hook

    The implementation of useRef is relatively simple, but its role is very important:

    function mountRef(initialValue) {
      const hook = mountWorkInProgressHook()
      const ref = { current: initialValue }
      hook.memoizedState = ref
      return ref
    }
    
    function updateRef() {
      const hook = updateWorkInProgressHook()
      return hook.memoizedState
    }
    

    The core idea of useRef is to create a mutable reference that remains unchanged throughout the lifetime of the component. This is why we can use useRef to store DOM nodes or any mutable value without triggering component re-rendering.

    During the construction and submission phases of the fiber tree, React will specially handle fibers with ref:

    function markRef(current, workInProgress) {
      const ref = workInProgress.ref
      if ((current === null && ref !== null) || (current !== null && current.ref !== ref)) {
        workInProgress.flags |= Ref
      }
    }
    

    This function will mark the fiber when the ref changes so that it can be processed during the commit phase.

    In the commit phase, React will first remove the old ref and then set the new ref:

    function commitAttachRef(finishedWork) {
      const ref = finishedWork.ref
      if (ref !== null) {
        const instance = finishedWork.stateNode
        let instanceToUse
        switch (finishedWork.tag) {
          case HostComponent:
            instanceToUse = getPublicInstance(instance)
            break
          default:
            instanceToUse = instance
        }
        if (typeof ref === 'function') {
          ref(instanceToUse)
        } else {
          ref.current = instanceToUse
        }
      }
    }
    

    OK, I understand you want to add something about shallow comparison dependencies in section 3. We could extend this section to explain in detail how React performs dependency comparisons, and how this affects the performance optimization of useMemo and useCallback. The following is the modified content:

    3. Implementation and dependency comparison mechanism of useMemo and useCallback

    The implementations of useMemo and useCallback are very similar, and they are both used for performance optimization. The core of these two Hooks is to compare dependencies to decide whether the value needs to be recalculated or the function re-created. Let's take a closer look at their implementation, especially the mechanisms that rely on comparisons:

    function mountMemo(nextCreate, deps) {
      const hook = mountWorkInProgressHook()
      const nextDeps = deps === undefined ? null : deps
      const nextValue = nextCreate()
      hook.memoizedState = [nextValue, nextDeps]
      return nextValue
    }
    
    function updateMemo(nextCreate, deps) {
      const hook = updateWorkInProgressHook()
      const nextDeps = deps === undefined ? null : deps
      const prevState = hook.memoizedState
      if (prevState !== null) {
        if (nextDeps !== null) {
          const prevDeps = prevState[1]
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            return prevState[0]
          }
        }
      }
      const nextValue = nextCreate()
      hook.memoizedState = [nextValue, nextDeps]
      return nextValue
    }
    

    Depend on comparison mechanism

    React uses the areHookInputsEqual function to do shallow comparisons of dependencies. The implementation of this function is roughly as follows:

    function areHookInputsEqual(nextDeps, prevDeps) {
      if (prevDeps === null) {
        return false
      }
      for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
        if (Object.is(nextDeps[i], prevDeps[i])) {
          continue
        }
        return false
      }
      return true
    }
    

    This function has the following key points:

    1. Shallow comparison: only compares the reference of each element in the dependent array, without performing deep comparison.

    2. Use Object.is: This method compares whether two values ​​​​are the same. It is similar to the === operator, but can correctly handle NaN and -0 situations.

    3. Length-sensitive: If the lengths of the old and new dependency arrays are different, false will be returned.

    The impact of shallow comparison

    The shallow comparison mechanism has an important impact on the use of useMemo and useCallback:

    1. Basic type dependence: For basic types such as numbers, strings, and Boolean values, shallow comparison can effectively detect changes.

    2. Object and array dependencies: Since only references are compared, if you pass in an object or array that is newly created every time it is rendered, it will be regarded as a dependency change even if the content does not change. For example:

      useMemo(() => expensiveCalculation(a, b), [{ a, b }]) // Not recommended
      

      Here, a new object is created for each rendering, causing the memo to become invalid. Should be changed to:

      useMemo(() => expensiveCalculation(a, b), [a, b]) // Recommended
      
    3. Functional dependencies: Similarly, if a dependency contains an inline function, a new function reference will be created with each rendering, causing optimization to fail.

    Best Practices

    In order to take full advantage of the optimization effects of useMemo and useCallback:

    1. Only include necessary dependencies: only include values that the calculation or callback function really depends on.

    2. Use basic type dependencies: Use basic types such as numbers and strings as dependencies as much as possible.

    3. Stable references: For object or function dependencies, consider using useMemo or useCallback to stabilize their references.

    4. Consider using useRef: For mutable values ​​that don’t need to trigger re-rendering, consider using useRef instead of making it a dependency.

    Summary

    The implementation of React Hooks involves complex internal mechanisms, including the construction, update and submission process of the fiber tree.

    Key points include:

    1. Execution timing and optimization mechanism of Effect Hooks (useEffect and useLayoutEffect)

    2. The non-triggering re-rendering feature of Ref Hook (useRef) and its application in DOM operations

    3. Dependency comparison and caching mechanism of Memo Hooks (useMemo and useCallback)

    Through these mechanisms, React achieves the ability to manage state and side effects in functional components while maintaining good performance and development experience. An in-depth understanding of these implementation details will help us better use Hooks in actual development and write higher-quality React code.

    Comments

    Join the conversation

    0 comments
    Sign in to comment

    No comments yet. Be the first to add one.