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

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(<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 fromuseReducer
- Class components: Calling
this.setState()
orthis.forceUpdate()
- Calling the ReactDOM
render()
method again (equivalent toforceUpdate()
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()
orDate.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 class component lifecycle method that lets you control whether the component should re-render.
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:
- React checks if the provider's value is a new reference
- If it's a new reference, React knows the value has changed
- 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:
- Use the React DevTools Profiler to see which components are rendering and why
- Look for components that render unnecessarily
- Apply optimization techniques like
React.memo()
,useMemo()
, anduseCallback()
appropriately - 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:
React Forget: An experimental compiler designed to automatically add memoization to components, potentially eliminating many unnecessary renders throughout the component tree.
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()
, anduseCallback()
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.