React Deep Dive

Chapter 5: The Commit Phase and Effects

In the previous chapters, we've seen how Fiber builds the workInProgress tree and how it manages priorities. Now, let's explore the final phase of the rendering process: the commit phase, where React applies changes to the DOM and handles effects.

The Three Sub-Phases of Commit

The commit phase is divided into three sub-phases, each with a specific responsibility:

  1. Before Mutation Phase

    • Snapshot the current state
    • Prepare for DOM updates
    • Schedule passive effects
  2. Mutation Phase

    • Apply DOM updates
    • Call unmount effects
    • Update refs
  3. Layout Phase

    • Call layout effects
    • Update refs again
    • Schedule passive effects

Here's how these phases are structured in React's code:

1function commitRoot(root) {
2  const finishedWork = root.finishedWork;
3  
4  // Before Mutation Phase
5  commitBeforeMutationEffects(finishedWork);
6  
7  // Mutation Phase
8  commitMutationEffects(finishedWork);
9  
10  // Switch the current and workInProgress trees
11  root.current = finishedWork;
12  
13  // Layout Phase
14  commitLayoutEffects(finishedWork);
15}

Understanding Effects

React has several types of effects, each with different timing and purposes:

  1. Layout Effects (Synchronous)

    • Run synchronously after DOM mutations
    • Block visual updates
    • Used for measurements and DOM updates
  2. Passive Effects (Asynchronous)

    • Run asynchronously after paint
    • Don't block visual updates
    • Used for subscriptions, data fetching, etc.

Here's how effects are structured in a fiber node:

1type Fiber = {
2  // ... other properties
3  flags: number,           // Effect flags
4  firstEffect: Fiber | null,  // First effect in the list
5  lastEffect: Fiber | null,   // Last effect in the list
6  nextEffect: Fiber | null,   // Next effect to process
7};

How useEffect Works

Let's break down how useEffect works under the hood:

1function updateEffect(create, deps) {
2  const hook = updateWorkInProgressHook();
3  const nextDeps = deps === undefined ? null : deps;
4  let destroy = undefined;
5
6  if (currentHook !== null) {
7      const prevEffect = currentHook.memoizedState;
8      destroy = prevEffect.destroy;
9      
10      if (nextDeps !== null) {
11          const prevDeps = prevEffect.deps;
12          if (areHookInputsEqual(nextDeps, prevDeps)) {
13              // Skip effect if deps haven't changed
14              pushEffect(NoHookEffect, create, destroy, nextDeps);
15              return;
16          }
17      }
18  }
19
20  // Schedule effect for commit phase
21  hook.memoizedState = pushEffect(
22      HookHasEffect | HookPassive,
23      create,
24      destroy,
25      nextDeps
26  );
27}

Effect Execution Flow

  1. During Render

    • Effects are collected in the effect list
    • Each effect is tagged with its type and priority
    • Dependencies are compared to determine if effect should run
  2. During Commit

    • Before Mutation: Schedule passive effects
    • Mutation: Run cleanup functions
    • Layout: Run layout effects
    • After Paint: Run passive effects

Here's a practical example:

1function Example() {
2  // Layout effect - runs synchronously after DOM updates
3  useLayoutEffect(() => {
4      // Measure DOM node
5      const height = elementRef.current.getBoundingClientRect().height;
6      // Update DOM immediately
7      elementRef.current.style.height = `${height}px`;
8  }, []);
9
10  // Passive effect - runs asynchronously after paint
11  useEffect(() => {
12      // Subscribe to data
13      const subscription = dataSource.subscribe();
14      // Cleanup subscription
15      return () => subscription.unsubscribe();
16  }, []);
17
18  return <div ref={elementRef}>Content</div>;
19}

Effect Flags and Priorities

React uses flags to track different types of effects:

1const EffectFlags = {
2  NoFlags: 0,
3  Placement: 1,        // New node
4  Update: 2,           // Update existing node
5  Deletion: 4,         // Remove node
6  Passive: 8,          // Passive effect
7  Layout: 16,          // Layout effect
8  Ref: 32,             // Ref update
9  Snapshot: 64,        // Snapshot effect
10};

Benefits of This Approach

  1. Predictable Updates

    • DOM updates happen in a consistent order
    • Effects run at the right time
    • Visual updates are synchronized
  2. Performance Optimization

    • Effects are batched when possible
    • Passive effects don't block rendering
    • Cleanup functions prevent memory leaks
  3. Developer Experience

    • Clear separation of concerns
    • Predictable effect timing
    • Easy to reason about updates

In the next chapter, we'll explore how React handles error boundaries and recovery from errors during the rendering process.