React|React operation principles and state management

The 2 Stages of React Rendering: Render Phase vs. Commit Phase

0
The 2 Stages of React Rendering: Render Phase vs. Commit Phase

Chris is scratching his head while debugging some React code.

"The console.log definitely printed, so why is the screen exactly the same? And why is the console logging everything twice?"

Many developers confuse the word "Rendering" with "Painting" (drawing to the screen). However, in the world of React, rendering does not strictly mean updating the screen. In fact, a render can occur without a single pixel changing on the screen.

Accurately understanding the process from when React calls a component to when it reflects changes on the screen is key to preventing unnecessary re-renders and predicting exactly when useEffect will fire. Today, we dig into the secrets of the Render Phase and the Commit Phase.

React's Order System

Let's compare React's rendering process to a restaurant order system.

  • Render Phase (Taking & Checking Orders): The customer (Component) picks a menu item, and the Chef (React) writes up the ticket. This is the stage where the Chef checks, "Is this order different from the previous one?"
  • Commit Phase (Cooking & Serving): The food is cooked according to the ticket and actually served to the table (DOM).
  • The crucial point is this: Just because an order ticket was written (Render Phase) does not guarantee that food will come out (Commit Phase). If the order content is exactly the same as before, the Chef doesn't start cooking.

    1. Render Phase

    This is the domain of "Calculation." React calls the component function, takes the returned JSX (Virtual DOM), and compares it with the result of the previous render. This process is called Reconciliation.

  • Tasks: Calling components, creating the Virtual DOM, Diffing (Comparison).
  • Characteristics: It must have No Side Effects. You should not call APIs or modify the DOM directly in this phase because it can be paused, aborted, or restarted by React at any time.
  • // Render Phase Example
    function UserProfile({ name }: { name: string }) {
      // 1. Component Body Execution (Render Phase)
      console.log('Render Phase: Calculating...'); 
      
      // 2. Returns JSX (This object is compared to the previous one)
      return <div>{name}</div>; 
    }

    Why does the console log twice? (Strict Mode)

    The reason console.log appears twice in development mode is due to React's Strict Mode. React intentionally calls the component twice to verify: "Is the Render Phase pure?" If the results differ between the two executions, it signals that there are side effects that need fixing.

    2. Commit Phase

    This is the domain of "Application." The "changes" calculated during the Render Phase are applied to the actual DOM.

  • Tasks: Creating/Deleting/Updating DOM nodes, updating refs.
  • Characteristics: This process happens Synchronously and cannot be interrupted. useLayoutEffect runs immediately after DOM mutation is complete, and useEffect runs after the browser paints the screen.
  • // Runs after Commit Phase
    useEffect(() => {
      console.log('After Commit Phase: Now executing side effects');
    }, []);

    The Complete Flow

    Let's trace the flow when Chris clicks a button to change state.

  • Trigger: setCount(1) is called.
  • Render Phase:
  • Commit Phase:
  • What if we update the state with the same value, setCount(0)?

    React compares them in the Render Phase and finds no changes. Consequently, React skips the Commit Phase. This saves the cost of DOM manipulation.

    Practical Code: Verifying Execution Order

    If you run the code below, in what order will the console logs appear?

    import React, { useState, useEffect, useLayoutEffect } from 'react';
    
    function CycleTest() {
      console.log('1. Render Phase');
    
      const [count, setCount] = useState(0);
    
      useLayoutEffect(() => {
        console.log('2. Commit Phase (Right after DOM update, Before Paint)');
      }, [count]);
    
      useEffect(() => {
        console.log('3. Commit Phase (After Paint)');
      }, [count]);
    
      return (
        <button onClick={() => setCount(c => c + 1)}>
          {count}
        </button>
      );
    }

    The result is 1 -> 2 -> 3.

    Specifically, useLayoutEffect runs before the browser paints the screen, so it is used when you need to measure or mutate the DOM layout without causing a visual flicker. (However, useEffect is sufficient for most cases.)

    Key Takeaways

  • Rendering $\neq$ Screen Update: Rendering is the process where React calculates changes. The actual screen update happens in the Commit Phase.
  • Render Phase: Calls components and compares changes (Diffing). Must be pure.
  • Commit Phase: Applies changes to the DOM. useEffect runs after this.
  • Optimization: If the Render Phase result is the same as before, React skips the Commit Phase to optimize performance.

  • Now that we know the ins and outs of rendering, it's time to look at how React transitions screens: Routing.

    Continuing in: "React Router and History API: How Do SPAs Switch Pages?"

    🔗 References

  • React Docs - Render and Commit
  • React Lifecycle Methods Diagram