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:
    
    ```javascript
    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:
    
    ```javascript
    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:
    
    ```javascript
    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:
    
    ```javascript
    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:
    
    ```javascript
    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:
    
    ```javascript
    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:
    
    ```javascript
    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:
    
    ```javascript
    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:
    
    ```javascript
    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:
    
    ```javascript
    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](http://xn--Object-vt9i248w.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:
        
        ```javascript
        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:
        
        ```javascript
        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.