A Comprehensive Guide to React Rendering Behavior

What is Rendering in React? Rendering is the process of React asking your components to describe what they want their section of the UI to look like, now, based on the current combination of props and state. Think of React components as chefs in a kitchen, preparing dishes based on specific recipes (props and state). React acts like a waiter, taking orders from customers and bringing them their meals. This process involves three key steps: Triggering a render (taking the guest's order) Rendering the component (preparing the order in the kitchen) Committing to the DOM (serving the order to the table) Step 1: Triggering a Render There are two main reasons why a component would render: Initial render - When your app first starts State updates - When a component's state (or one of its ancestors' state) changes Initial Render When your app starts, you trigger the initial render by calling createRoot with the target DOM node, and then calling its render method with your component: import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('root')); root.render(); Re-renders from State Updates After the initial render, you can trigger additional renders by updating component state with a setter function. Updating state automatically queues a render. Here are the ways to queue a re-render: Function components: Using state setters from useState or dispatches from useReducer Class components: Calling this.setState() or this.forceUpdate() Calling the ReactDOM render() method again (equivalent to forceUpdate() on the root component) Step 2: React Renders Your Components After a render is triggered, React calls your components to figure out what to display. "Rendering" is React calling your components. During this phase: For initial render, React starts with the root component For subsequent renders, React starts with the component that had its state updated The rendering process is recursive - React will render the component that triggered the update, then its children, then their children, and so on. The Important Rule of React Rendering React's default behavior is that when a parent component renders, React will recursively render all child components inside of it! This means: When a parent renders, all children automatically render too React does not care whether "props changed" - children render unconditionally when parents render Rendering itself is not bad - it's how React determines what needs to change Component Trees and Rendering Example Consider a component tree of A > B > C > D, where a user clicks a button in B that increments a counter: We call setState() in B, which queues a re-render of B React starts the render pass from the top of the tree React sees A is not marked for update and moves past it React sees B is marked for update and renders it Since B rendered, React moves downwards and renders C too Since C rendered, React moves downwards and renders D too Render vs. DOM Update A key part to understand is that "rendering" is not the same thing as "updating the DOM", and a component may be rendered without any visible changes happening as a result. When React renders a component: The component might return the same render output as last time, so no changes are needed In Concurrent Rendering, React might render a component multiple times, but throw away the render output if other updates invalidate the current work Render Phase vs. Commit Phase React divides rendering work into two phases: Render Phase: Contains all the work of rendering components and calculating changes Commit Phase: The process of applying those changes to the DOM After the commit phase, React updates all refs to point to the DOM nodes and component instances, then synchronously runs lifecycle methods and useLayoutEffect hooks. After a short timeout, React runs all useEffect hooks in what's known as the "Passive Effects" phase. Rules of React Rendering One of the primary rules of React rendering is that rendering must be "pure" and not have any side effects! Render logic must NOT: Mutate existing variables and objects Create random values like Math.random() or Date.now() Make network requests Queue state updates Render logic MAY: Mutate objects that were newly created during rendering Throw errors "Lazy initialize" data that hasn't been created yet, such as a cached value Performance Optimization Techniques While renders are a normal part of React's workflow, sometimes we want to avoid unnecessary renders to improve performance. Component Render Optimization APIs React provides three main APIs to skip rendering components when appropriate: React.memo(): A higher-order component that prevents re-renders when props haven't changed. Works for both function and class components. shouldComponentUpdate: A

May 4, 2025 - 08:16
 0
A Comprehensive Guide to React Rendering Behavior

What is Rendering in React?

Rendering is the process of React asking your components to describe what they want their section of the UI to look like, now, based on the current combination of props and state.

Think of React components as chefs in a kitchen, preparing dishes based on specific recipes (props and state). React acts like a waiter, taking orders from customers and bringing them their meals. This process involves three key steps:

  1. Triggering a render (taking the guest's order)
  2. Rendering the component (preparing the order in the kitchen)
  3. Committing to the DOM (serving the order to the table)

Step 1: Triggering a Render

There are two main reasons why a component would render:

  1. Initial render - When your app first starts
  2. State updates - When a component's state (or one of its ancestors' state) changes

Initial Render

When your app starts, you trigger the initial render by calling createRoot with the target DOM node, and then calling its render method with your component:

import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

Re-renders from State Updates

After the initial render, you can trigger additional renders by updating component state with a setter function. Updating state automatically queues a render.

Here are the ways to queue a re-render:

  • Function components: Using state setters from useState or dispatches from useReducer
  • Class components: Calling this.setState() or this.forceUpdate()
  • Calling the ReactDOM render() method again (equivalent to forceUpdate() on the root component)

Step 2: React Renders Your Components

After a render is triggered, React calls your components to figure out what to display. "Rendering" is React calling your components.

During this phase:

  • For initial render, React starts with the root component
  • For subsequent renders, React starts with the component that had its state updated

The rendering process is recursive - React will render the component that triggered the update, then its children, then their children, and so on.

The Important Rule of React Rendering

React's default behavior is that when a parent component renders, React will recursively render all child components inside of it!

This means:

  • When a parent renders, all children automatically render too
  • React does not care whether "props changed" - children render unconditionally when parents render
  • Rendering itself is not bad - it's how React determines what needs to change

Component Trees and Rendering Example

Consider a component tree of A > B > C > D, where a user clicks a button in B that increments a counter:

  1. We call setState() in B, which queues a re-render of B
  2. React starts the render pass from the top of the tree
  3. React sees A is not marked for update and moves past it
  4. React sees B is marked for update and renders it
  5. Since B rendered, React moves downwards and renders C too
  6. Since C rendered, React moves downwards and renders D too

Render vs. DOM Update

A key part to understand is that "rendering" is not the same thing as "updating the DOM", and a component may be rendered without any visible changes happening as a result. When React renders a component:

  • The component might return the same render output as last time, so no changes are needed
  • In Concurrent Rendering, React might render a component multiple times, but throw away the render output if other updates invalidate the current work

Render Phase vs. Commit Phase

React divides rendering work into two phases:

  1. Render Phase: Contains all the work of rendering components and calculating changes
  2. Commit Phase: The process of applying those changes to the DOM

After the commit phase, React updates all refs to point to the DOM nodes and component instances, then synchronously runs lifecycle methods and useLayoutEffect hooks.

After a short timeout, React runs all useEffect hooks in what's known as the "Passive Effects" phase.

Rules of React Rendering

One of the primary rules of React rendering is that rendering must be "pure" and not have any side effects!

Render logic must NOT:

  • Mutate existing variables and objects
  • Create random values like Math.random() or Date.now()
  • Make network requests
  • Queue state updates

Render logic MAY:

  • Mutate objects that were newly created during rendering
  • Throw errors
  • "Lazy initialize" data that hasn't been created yet, such as a cached value

Performance Optimization Techniques

While renders are a normal part of React's workflow, sometimes we want to avoid unnecessary renders to improve performance.

Component Render Optimization APIs

React provides three main APIs to skip rendering components when appropriate:

  1. React.memo(): A higher-order component that prevents re-renders when props haven't changed. Works for both function and class components.

  2. shouldComponentUpdate: A class component lifecycle method that lets you control whether the component should re-render.

  3. React.PureComponent: A base class that implements a shallow comparison of props and state to determine if re-rendering is necessary.

These approaches all use "shallow equality" comparison to check if props or state have changed between renders.

How to Use Memoization Effectively

For function components, React provides hooks to help reuse the same references:

  • useMemo: For memoizing calculated values or objects
  • useCallback: Specifically for memoizing callback functions

Example of effective memoization:

function OptimizedComponent() {
  const [counter1, setCounter1] = useState(0);
  const [counter2, setCounter2] = useState(0);

  // This element stays the same reference if counter2 changes,
  // preventing unnecessary re-renders of ExpensiveChild
  const memoizedElement = useMemo(() => {
    return <ExpensiveChildComponent data={counter1} />;
  }, [counter1]);

  return (
    <div>
      <button onClick={() => setCounter1(counter1 + 1)}>Counter 1</button>
      <button onClick={() => setCounter2(counter2 + 1)}>Counter 2</button>
      {memoizedElement}
    </div>
  );
}

Should You Memoize Everything?

The answer is no - memoization itself has a cost. You should only optimize components when:

  • The component renders often with the same props
  • The rendering logic is expensive
  • You've identified a performance problem through profiling

Render Batching and Timing

React applies an optimization called "render batching." This is when multiple state updates result in a single render pass being queued and executed.

In React 18+, all updates queued within a single event loop tick are automatically batched together, reducing the total number of renders.

State Updates, Closures, and the "Snapshot" Model

A common confusion point in React:

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    console.log(count); // This will log the "old" value, not the updated one
  };

  return <button onClick={handleClick}>Increment</button>;
}

The reason this happens is that function components capture the state values in their closures. Each render has its own "snapshot" of state, and that snapshot can't see future updates.

Context and Rendering Behavior

React's Context API allows values to be passed down through the component tree without prop drilling.

Context is not a "state management" tool. You have to manage the values that are passed into context yourself, typically by keeping data in React component state.

How Context Updates Affect Rendering

When a context provider receives a new value:

  1. React checks if the provider's value is a new reference
  2. If it's a new reference, React knows the value has changed
  3. All components that consume that context need to be updated, even if they only use part of the context value

Optimizing Context Performance

The React component right under your Context Provider should probably use React.memo.

This prevents state updates in the parent component from causing all components to re-render - only the sections where context is actually used will re-render.

Measuring and Improving Performance

To identify and fix performance issues:

  1. Use the React DevTools Profiler to see which components are rendering and why
  2. Look for components that render unnecessarily
  3. Apply optimization techniques like React.memo(), useMemo(), and useCallback() appropriately
  4. Always test performance in production builds, not development mode

Immutability and Rendering

State updates in React should always be done immutably. Mutating state can lead to components not rendering when expected and confusion about when data actually updated.

When updating objects or arrays in state:

// ❌ BAD - mutating state directly
const handleClick = () => {
  todos[3].completed = true;
  setTodos(todos); // This won't trigger a re-render!
};

// ✅ GOOD - creating a new reference
const handleClick = () => {
  const newTodos = [...todos];
  newTodos[3].completed = true;
  setTodos(newTodos);
};

Future React Improvements

The React team is working on several improvements that may change how we think about rendering:

  1. React Forget: An experimental compiler designed to automatically add memoization to components, potentially eliminating many unnecessary renders throughout the component tree.

  2. Context Selectors: A proposed API that would allow components to selectively subscribe to only parts of a context value, making Context more efficient for larger state.

Summary

  • React always renders components recursively by default
  • Rendering itself is not a problem - it's how React knows what to update
  • React only updates the DOM when necessary after comparing render outputs
  • Use memoization techniques like React.memo(), useMemo(), and useCallback() to optimize performance when needed
  • Keep state updates immutable to ensure proper rendering
  • Context is useful for passing values down the tree but requires careful optimization

Understanding React's rendering behavior helps you build more efficient applications and debug rendering issues more effectively.