Table of Contents
Overview
In this blog post, we’ll cover the basic concepts of what React rendering is and how React handles component rendering.
1. What is Rendering?
Definition of Rendering
Rendering is “the process where React asks components to describe what part of the UI they want to display and how, based on the current combination of props and state.”
React traverses the component tree during rendering and performs the following:
- Finds components that have been flagged as needing updates.
- Calls
FunctionComponent(props)orclassComponentInstance.render()on flagged components to check if DOM updates are needed. - Saves the output from step 2 for the next stage (Commit Phase).
Rendering Output Examples
For components with children, the rendering result looks like this:
// JSX:
return <MyComponent a={42} b="testing">Text here</MyComponent>
// Gets transformed into:
return React.createElement(MyComponent, {a: 42, b: "testing"}, "Text Here")
// Which produces a React element object:
{type: MyComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]}
For components without children:
// A working component
function Greeting({ name, age }) {
return <div>Hello {name}, you are {age} years old</div>;
}
function App() {
// This JSX...
return <Greeting name="John" age={25} />;
}
// ...produces this object:
{ type: Greeting, props: { name: "John", age: 25 } }
Note: The React team has recently been avoiding the term Virtual DOM.
When people hear Virtual DOM, they imagine an HTML DOM Tree and think React keeps the same thing in memory. But this is not the case.
The React team uses the term Value UI. In React, UI elements can be treated as Values just like Strings or Arrays. This means you can store these values in variables, pass them around, or control them with JavaScript.
React’s Virtual DOM deals not with a DOM tree, but with a component tree (a Fiber tree, a tree made of JavaScript Objects).
2. Render Phase and Commit Phase
React divides the process of displaying the screen into two conceptual phases.
Render Phase
The Render Phase performs the following:
- Renders components and calculates changes
- Compares the new component tree with the existing tree
- Collects DOM changes that are needed through a process called “reconciliation”
Commit Phase
The Commit Phase performs the following:
- Applies updates to the DOM
- All changes are applied synchronously
After DOM Update
After the DOM update (after displaying on screen), a phase called the Passive Effects Phase runs.
- Updates ref references.
- Class components: Synchronously executes
componentDidMountandcomponentDidUpdate. Function components: Executes theuseLayoutEffecthook. - Function components: Then executes the
useEffecthook.
Important Point
“Rendering” ≠ “DOM Update”
Components may be rendered but not update the DOM in the following cases:
- When the component returns the same output
- When React discards work during concurrent rendering
3. How Does React Handle Rendering?
In React, various mechanisms trigger re-rendering, and these triggered renders are added to a queue for processing.
Re-rendering is triggered in the following cases:
Function Components:
useStatesetteruseReducerdispatch
Class Components:
this.setState()this.forceUpdate()
Others:
- Top-level ReactDOM
render(<App>)call useSyncExternalStoreupdates- https://react.dev/reference/react/useSyncExternalStore
useSyncExternalStoreis a React hook that enables subscribing to external stores.
Function components don’t have forceUpdate, but you can implement its behavior like this:
const [, forceRender] = useReducer((c) => c + 1, 0);
4. Standard Rendering Behavior
The Most Important Rule
In React, when a parent is rendered, all child components within it are recursively rendered.
Example: A > B > C > D
- The user clicks a button in B.
setState()adds B’s re-render to the queue.- React starts searching (Render Pass) from the root.
- A has no change flag, so it passes through.
- B has a change flag, so it renders. B returns
C. - C originally has no change flag, but since B was rendered, React also renders C. C returns
D. - D also originally has no change flag, but since C was rendered, React also renders D.
Important Point
“React doesn’t care whether props have changed. When a parent is rendered, it unconditionally renders child components.”
For example, if you call
setState()in the root<App>, all components in the tree will be re-rendered.React renders to determine which DOM changes are needed. (It doesn’t render because DOM changes are needed.)
5. React’s Rendering Rules
Rendering Must Be “Pure”
Rendering should have no side effects.
What you should NOT do in render logic:
- Mutate existing variables/objects
- Generate random values (
Math.random(),Date.now()) - Make network requests
- Queue state updates
What you CAN do in render logic:
- Mutate newly created objects
- Throw errors
- Lazy-initialize data that hasn’t been created yet
Example: Impure Rendering (X)
let renderCount = 0; // External variable
function ImpureComponent() {
renderCount++; // ❌ Mutates existing variable
// ❌ Network request during rendering
fetch('/api/data').then((data) => console.log(data));
return <div>Render #{renderCount}</div>;
}
// ❌ Same function execution, but results change.
function RandomNumber() {
const random = Math.random(); // ❌ Generates random value
return <p>{random}</p>;
}
Example: Pure Rendering (O)
function PureComponent({ items }) {
// ✅ Creates and modifies a new array
const sortedItems = [...items].sort((a, b) => a.name.localeCompare(b.name));
// ✅ Conditional error
if (!items || items.length === 0) {
throw new Error('Items are required');
}
// ✅ Lazy initialization (runs only once)
const [data] = useState(() => {
return expensiveCalculation();
});
return (
<ul>
{sortedItems.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// Side effects should be handled in event handlers or useEffect
function ProperComponent() {
const [data, setData] = useState(null);
// ✅ Network requests in useEffect
useEffect(() => {
fetch('/api/data')
.then((res) => res.json())
.then(setData);
}, []);
// ✅ Random values in event handlers
const handleClick = () => {
const randomValue = Math.random();
console.log('Random:', randomValue);
};
return (
<div>
<button onClick={handleClick}>Generate Random</button>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
6. Fiber Objects
What is Fiber?
React has an internal data structure for tracking component instances. This structure is the “Fiber” object.
- Fiber code: https://github.com/facebook/react/blob/v17.0.0/packages/react-reconciler/src/ReactFiber.new.js#L47-L174
During the rendering process, React traverses this tree of Fiber objects, calculates new rendering results, and generates an updated tree.
What Fiber Objects Contain
- Component type information
- Current props and state
- Pointers to parent, sibling, and child components
- Internal rendering metadata
Fiber (Simplified)
const fiberNode = {
tag: 0, // Indicates the type of component (function component, class, DOM element, etc.)
type: MyComponent, // The actual React component function, or tag name ('div', etc.)
key: null, // key attribute (for list identification)
stateNode: null, // The actual DOM node or class instance
// Pointers for forming the Fiber tree structure
return: null, // Points to the parent Fiber
child: null, // Points to the first child Fiber
sibling: null, // Points to the next sibling Fiber
pendingProps: { name: 'React' }, // Props being updated (not yet applied)
memoizedProps: null, // Props used in the previous render
memoizedState: null, // State managed by useState, useReducer, etc.
alternate: null, // Points to the previous or next Fiber (double buffering structure)
};
Think of React components as a facade over React’s Fiber objects.
7. Fiber and Rendering
React tries to maximize reuse of the existing component tree and DOM structure for efficient re-rendering.
When React needs to render the same type of component or HTML node at the same position in the tree, it tries to reuse the existing one instead of creating a new component instance.
In other words, when a request comes in to render the same type of component at the same position, React keeps the existing component instance.
- For class components, it uses the exact same instance as the existing component instance.
- For function components, since they don’t have instances like class components, Fiber acts as a substitute for instances, representing “this type of component is displayed here.”
Component Type Comparison
React compares elements using === reference comparison on the type field inside Fiber.
When the element type changes (e.g., from <ComponentA> to <ComponentA'>), React destroys the entire existing tree section and recreates it from scratch.
Don’t Create New Components During Rendering!
If you create new components during rendering, a new component is created every time it renders, preventing React from managing rendering efficiently.
Bad Example (X):
function ParentComponent() {
// A new component type is created every render!
function ChildComponent() {
return <div>Hi</div>;
}
return <ChildComponent />;
}
Therefore, you should define child components separately so React can manage rendering efficiently.
function ChildComponent() {
return <div>Hi</div>;
}
function ParentComponent() {
return <ChildComponent />;
}
8. Keys and Rendering
What is a Key?
In React, key is an instruction (guideline) to React, not an actual prop (it is not passed to child components).
React treats keys as identifiers for identifying specific instances of a component type.
Keys are especially important when rendering lists with mutable data.
Mutable Data and Keys
“Keys should use IDs from the data whenever possible. Avoid using array indices!”
Using array indices can cause the following problems:
When deleting items 6-7 from a 10-item list with index keys and adding 3 new elements,
- [0, 1, 2, 3, 4, 5,
6, 7, 8, 9] → [0, 1, 2, 3, 4, 5, 6, 7] → [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] React sees [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - React simply thinks one item was added and reuses the component instances for 6 and 7, which should actually have been deleted.
- The same instances are used but end up holding completely different data.
- Data and components may not match.
Therefore, you should use unique IDs like this:
// ✅ Use unique IDs
todos.map((todo) => <TodoListItem key={todo.id} todo={todo} />);
Other Uses for Keys
Keys can be used on any component, and by updating the key, you can force a component to be recreated. (Replacing the instance)
Example: Resetting a Component with Key
function ProfileForm({ userId }) {
const [formData, setFormData] = useState({ name: '', email: '' });
// The form resets every time the user changes (due to key change)
// No need to reset with useEffect!
return (
<form>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Name"
/>
<input
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="Email"
/>
</form>
);
}
function UserProfile() {
const [selectedUserId, setSelectedUserId] = useState(1);
return (
<div>
<button onClick={() => setSelectedUserId(1)}>User 1</button>
<button onClick={() => setSelectedUserId(2)}>User 2</button>
{/* Component is completely recreated when key changes */}
<ProfileForm key={selectedUserId} userId={selectedUserId} />
</div>
);
}
9. Render Batching
Render batching means that multiple setState() calls are queued with a slight delay and rendered as a single render pass.
Before React 17
In versions before React 17, only event handlers were batch-processed. Therefore, updates outside of event handlers were executed individually:
- Inside
setTimeout - After
await - Regular JS handlers
React 18 and Later
All updates within an Event Loop Tick are now automatically batched.
Render Batching Example
const [counter, setCounter] = useState(0);
const handleClick = async () => {
setCounter(0);
setCounter(1);
const data = await fetchSomeData();
setCounter(2);
setCounter(3);
};
- React 17: 3 render passes (first 2 batched, each one after await individually)
- React 18: 2 render passes (calls 0-1 together, calls 2-3 together after await)
10. Async Rendering, Closures, and State Snapshots
There are cases where not being aware of closures can cause problems:
function MyComponent() {
const [counter, setCounter] = useState(0);
const handleClick = () => {
setCounter(counter + 1);
console.log(counter); // ❌ Logs the original value
};
}
This happens for the following reasons:
handleClickis a closure that references variables that existed at the time it was defined.- During this render,
counterhas a specific value. setCounter()adds a future render to the queue.- The future render creates a new
countervariable and a newhandleClickfunction. - But the current copy cannot reference that new value.
Core Concept
“These state variables are snapshots from that point in time.”
Here is an example of closures and state snapshots:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// ❌ This doesn't work as expected
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// count is still this render's value (e.g., 0)
// Even calling 3 times, the result is 1 (0 + 1 = 1, all 3 times)
};
const handleClickCorrect = () => {
// ✅ Use functional updates
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
// Each update uses the previous value, so the result increases by 3
};
const handleAsyncLog = () => {
setCount(count + 1);
setTimeout(() => {
// ❌ Logs the count value at click time (not the new value)
console.log('Count in timeout:', count);
}, 3000);
};
const handleAsyncLogCorrect = () => {
setCount((prev) => {
const newCount = prev + 1;
setTimeout(() => {
// ✅ Logs the updated value
console.log('Count in timeout:', newCount);
}, 3000);
return newCount;
});
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>❌ +3 (doesn't work)</button>
<button onClick={handleClickCorrect}>✅ +3 (correct way)</button>
<button onClick={handleAsyncLog}>❌ Log (old value)</button>
<button onClick={handleAsyncLogCorrect}>✅ Log (new value)</button>
</div>
);
}
Summary
- Rendering results are in the form of JavaScript objects.
- Virtual DOM means a component tree, not an HTML DOM tree.
- The important point in React is not Virtual DOM, but Value UI.
- React has a Render Phase and a Commit Phase for rendering the screen.
- “Rendering” ≠ “DOM Update”
- When a parent is rendered, React recursively renders all child components within it.
- React doesn’t care whether props have changed. When a parent is rendered, it unconditionally renders child components.
- React renders to determine which DOM changes are needed.
- Rendering must be “pure.”
- During the rendering process, React traverses the Fiber object tree, calculates new rendering results, and generates an updated tree.
- You must not create new component types during rendering.
Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!
App promotion
Deku.Deku created the applications with Flutter.If you have interested, please try to download them for free.