Skip to main content

State Updates and Re-rendering: Understanding the Component Re-render Cycle #51

📖 Introduction

Following our deep dive into the useState hook in The useState Hook: A Deep Dive (Part 2), we now turn to a critical consequence of updating state: re-rendering. Understanding when and why React re-renders a component is one of the most fundamental skills for building efficient, bug-free applications. It's the key to unlocking high performance and mastering control over your UI's behavior.


📚 Prerequisites

Before we begin, please ensure you have a solid grasp of the following concepts:

  • JavaScript Functions and Closures
  • React Components, Props, and State (specifically useState)
  • The concept of the Virtual DOM from The Virtual DOM Explained

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • Foundational Theory: What a "re-render" actually is and the three phases of React's update pipeline: Render, Commit, and Passive Effects.
  • Core Triggers: The four primary reasons a component re-renders: state changes, parent re-renders, context changes, and hook updates.
  • Practical Application: Visualizing the re-render cycle with a practical example to see how state changes in one component affect its children.
  • Advanced Concepts: Understanding how React batches state updates for performance and how to queue multiple updates correctly.
  • Best Practices & Anti-Patterns: Recognizing common mistakes that lead to unnecessary re-renders and learning patterns to optimize performance.

🧠 Section 1: The Core Concepts of Re-rendering

At its heart, a re-render is the process of React calling your component's function again to get a new set of instructions for what the UI should look like. This doesn't mean the entire DOM is thrown away and rebuilt. Instead, React intelligently compares the new instructions (the new Virtual DOM) with the previous ones and surgically updates only the parts of the actual DOM that have changed.

This entire process can be broken down into a pipeline:

  1. Trigger: An event occurs (like a user click) that calls a state setter function (setNumber(1)).
  2. Render Phase (The "Thinking" Phase): React gets triggered to re-render. It walks the component tree from the component that had its state updated downwards, calling the functions of affected components to create a new, updated Virtual DOM tree. This phase is where React "thinks" about what has changed. It's asynchronous and can be paused.
  3. Commit Phase (The "Doing" Phase): React takes the new Virtual DOM from the render phase, compares it (diffs it) with the old one, and applies only the necessary changes to the real DOM. This phase is synchronous and cannot be interrupted. This is when the user sees the changes on the screen.
  4. Passive Effects Phase: After the changes are committed to the DOM, React runs any useEffect hooks.

💻 Section 2: What Triggers a Re-render?

There are four main reasons a component will re-render. Understanding them is key to controlling your application's performance.

2.1 - State Changes

This is the most common trigger. When you update a component's state using its setter function (e.g., setCount(c => c + 1)), you are telling React, "The data for this component has changed, please schedule a re-render to reflect this change."

// code-block-1.jsx
import React, { useState } from 'react';

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

console.log('Counter component is rendering...');

return (
<div>
<h1>Count: {count}</h1>
{/* This onClick handler triggers a state change, which schedules a re-render */}
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}

export default Counter;

Step-by-Step Code Breakdown:

  1. const [count, setCount] = useState(0);: We initialize a state variable count to 0.
  2. console.log(...): We add this log to visually confirm in the browser's console every time this component function is called (i.e., every time it renders).
  3. onClick={() => setCount(count + 1)}: When the button is clicked, we call setCount. This doesn't change count immediately. Instead, it tells React: "I have a new value for count. Please queue a re-render of the Counter component."
  4. Re-render: React processes the state update, calls the Counter function again, sees the new value of count, and updates the <h1> in the DOM.

2.2 - Parent Re-renders

When a component re-renders, it re-renders all of its children by default. This is a crucial concept. It doesn't matter if the props passed to the child have changed or not.

// code-block-2.jsx
import React, { useState } from 'react';

function Child() {
console.log('Child component is rendering...');
return <p>I am the child component.</p>;
}

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

console.log('Parent component is rendering...');

return (
<div>
<h1>Parent Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment Parent
</button>
<Child />
</div>
);
}

export default Parent;

Walkthrough:

  • When you click the "Increment Parent" button, the Parent component's state changes, so it re-renders.
  • Because Parent re-renders, it also calls the Child component function again, causing it to re-render too.
  • You will see logs from both Parent and Child in the console, even though the Child component has no state and its props haven't changed.

2.3 - Context Changes & Hook Updates

We will cover these in later chapters, but it's important to know they exist:

  • Context Changes: If a component consumes a React Context, it will re-render whenever that Context's value changes.
  • Hook Updates: If a custom hook used by a component has internal state that updates, it will trigger the host component to re-render.

🛠️ Section 3: Understanding State Batching

You might think that calling setState three times would cause three re-renders. But React is smarter than that. It batches state updates that happen in the same event handler to improve performance.

The Goal: To understand how React processes multiple state updates within a single event.

// project-example.jsx
import React, { useState } from 'react';

function TripleCounter() {
const [number, setNumber] = useState(0);

function handleClick() {
// All three of these calls are "batched" together.
// React sees number as 0 for all of them.
setNumber(number + 1); // Queues a render with number = 1
setNumber(number + 1); // Queues a render with number = 1
setNumber(number + 1); // Queues a render with number = 1

// The UI will only update once, and the final value will be 1, not 3.
}

function handleCorrectClick() {
// To correctly queue multiple updates, we use the "updater function" form.
// React processes these functions in order during the next render.
setNumber(n => n + 1); // Queues a function: "take the pending state and add 1"
setNumber(n => n + 1); // Queues another function
setNumber(n => n + 1); // Queues a third function

// The UI will update once, and the final value will be 3.
}

return (
<div className="project-feature">
<h2>Number: {number}</h2>
<button onClick={handleClick}>Increment (Incorrectly)</button>
<button onClick={handleCorrectClick}>Increment (Correctly)</button>
</div>
);
}

export default TripleCounter;

Walkthrough:

  1. Incorrect Button: In handleClick, React sees that number is 0 for the entire duration of the function call. It queues three updates, but each one says "set the number to 0 + 1". The last one wins, and after the event handler finishes, React performs a single re-render with number as 1.
  2. Correct Button: In handleCorrectClick, we pass a function (n => n + 1) to setNumber. This is an updater function. React queues these functions. During the next render, it processes the queue:
    • Starts with state 0.
    • Runs the first function: 0 + 1 = 1.
    • Runs the second function with the result of the first: 1 + 1 = 2.
    • Runs the third function with the result of the second: 2 + 1 = 3.
    • The final state is 3, and React performs a single re-render with that value.

🚀 Section 4: Advanced Techniques and Performance

The key to performance is preventing unnecessary re-renders. If a component's visual output doesn't need to change, it shouldn't re-render.

  • Memoization: In later articles, we will explore tools like React.memo, useMemo, and useCallback. These are "memoization" techniques that let you skip re-renders by telling React: "If the props to this component haven't changed, don't bother re-rendering it."
  • Composition: As we saw in the Parent/Child example, a parent re-render triggers a child re-render. A powerful pattern to avoid this is "composition", where you pass components as props (children), which can prevent them from being affected by the parent's render cycle.

✨ Section 5: Best Practices and Anti-Patterns

Best Practices:

  • Keep State Local: Hold state in the lowest possible component in the tree that needs it.
  • Lift State Up When Necessary: When multiple children need to share or coordinate state, "lift" that state to their closest common parent.

Anti-Patterns (What to Avoid):

  • Don't Create Components in the Render Function:
    // ANTI-PATTERN: Don't do this!
    function Parent() {
    const [count, setCount] = useState(0);

    // The 'Child' component function is redefined on every single render of Parent.
    // React will see it as a brand new component type each time, destroying and
    // re-creating it, which is terrible for performance and loses all child state.
    function Child() {
    return <p>I am a child</p>;
    }

    return (
    <div>
    <button onClick={() => setCount(c => c + 1)}>Update</button>
    <Child />
    </div>
    );
    }
    Correction: Always define components at the top level of your module or export them from their own file.

💡 Conclusion & Key Takeaways

Congratulations! You've just unpacked one of React's most fundamental and often misunderstood concepts. Mastering the re-render cycle is the difference between writing apps that simply work and writing apps that are fast, efficient, and predictable.

Let's summarize the key takeaways:

  • Re-renders are triggered by state changes (and a few other things). When state is updated, React calls the component function again to get the new UI description.
  • Renders cascade down. When a parent component re-renders, all of its children re-render by default.
  • React batches state updates. Multiple setState calls within the same event handler are grouped into a single re-render for performance.
  • Use updater functions for queued updates. When you need to update state based on its previous value multiple times in one go, use the functional form: setState(prevState => prevState + 1).

Challenge Yourself: Take the Parent/Child example from Section 2.2. Can you modify it so that the Child component does not re-render when the parent's state changes? (Hint: This is an advanced challenge that requires a technique we've only briefly mentioned: React.memo. Try looking it up!)


➡️ Next Steps

Now that you understand when a component updates, we can explore what we can do with that state. In the next article, "Working with State: Numbers, Strings, and Booleans", we will dive into practical examples of managing simple state types to build interactive UIs.

Thank you for your dedication. Stay curious, and happy coding!


glossary

  • Re-render: The process where React calls a component's function again to determine if the UI needs to be updated.
  • Render Phase: The first step in an update, where React creates a new Virtual DOM tree by calling the necessary component functions. It is asynchronous.
  • Commit Phase: The second step, where React compares the new Virtual DOM with the old one and applies the changes to the actual DOM. It is synchronous.
  • Batching: React's process of grouping multiple state updates that occur in the same event handler into a single re-render to optimize performance.
  • Updater Function: A function passed to a state setter (e.g., setNumber(n => n + 1)) that receives the pending state and returns the new state, used for safely queueing multiple updates.

Further Reading