A deep dive into how React works under the hood, with visual diagrams and practical examples.
-
- JSX Transformation Example
- React.createElement Signature
- Complex Example
- What React.createElement Returns
- JSX Compilation Flow
-
- Real DOM vs Virtual DOM Comparison
- Virtual DOM Structure
- Example: Virtual DOM Object
-
- Diffing Heuristics
- Type Change Example
- Same Type Diffing
-
- Without Keys (Bad)
- With Keys (Good)
- Key Rules
-
- High-Level Reconciliation Flow
- Step-by-Step Example
- Reconciliation Phases
-
- What is a Fiber?
- Fiber Tree Structure
- Work Loop
- Why Fiber?
-
- Complete React Update Cycle
- Real-World Example
-
- Best Practices
JSX is syntactic sugar that gets transformed into React.createElement calls during the build process.
// What you write:
const element = <div className="container">Hello World</div>;
// What it becomes:
const element = React.createElement(
'div',
{ className: 'container' },
'Hello World'
);React.createElement(type, props, ...children)- type: String (HTML tag) or Component (function/class)
- props: Object with properties (null if none)
- children: Child elements or text
// JSX:
<div className="card">
<h1>Title</h1>
<p>Description</p>
<Button onClick={handleClick}>Click me</Button>
</div>
// Transformed to:
React.createElement(
'div',
{ className: 'card' },
React.createElement('h1', null, 'Title'),
React.createElement('p', null, 'Description'),
React.createElement(
Button,
{ onClick: handleClick },
'Click me'
)
);It returns a React Element - a plain JavaScript object:
{
type: 'div',
props: {
className: 'card',
children: [
{ type: 'h1', props: { children: 'Title' } },
{ type: 'p', props: { children: 'Description' } },
{ type: Button, props: { onClick: fn, children: 'Click me' } }
]
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element')
}graph LR
A[JSX Code] -->|Babel/Compiler| B[React.createElement calls]
B -->|Runtime| C[React Elements]
C -->|React Renderer| D[Fiber Nodes]
D -->|Reconciliation| E[DOM Updates]
The Virtual DOM is React's lightweight representation of the actual DOM.
| Aspect | Real DOM | Virtual DOM |
|---|---|---|
| Nature | Browser's actual DOM tree | JavaScript object representation |
| Updates | Expensive (triggers reflow/repaint) | Cheap (just object manipulation) |
| Speed | Slow for frequent updates | Fast diffing and batching |
| Memory | Heavy | Lightweight |
graph TD
A[Virtual DOM Tree] --> B[div#root]
B --> C[div.container]
C --> D[h1 'Hello']
C --> E[ul]
E --> F[li 'Item 1']
E --> G[li 'Item 2']
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#ffe1e1
// Virtual DOM representation
{
type: 'div',
props: {
className: 'container',
children: [
{
type: 'h1',
props: { children: 'Hello' }
},
{
type: 'ul',
props: {
children: [
{ type: 'li', props: { children: 'Item 1' } },
{ type: 'li', props: { children: 'Item 2' } }
]
}
}
]
}
}React uses a heuristic O(n) algorithm instead of the traditional O(n³) tree diffing.
React makes two assumptions:
- Different types produce different trees - If element type changes, rebuild from scratch
- Keys identify which children have changed - Use keys to match elements across renders
graph LR
subgraph "Old Tree"
A1[div] --> B1[span 'Hello']
end
subgraph "New Tree"
A2[p] --> B2[span 'Hello']
end
A1 -.->|Type Changed| A2
style A1 fill:#ffcccc
style A2 fill:#ccffcc
Result: Entire subtree is destroyed and rebuilt because div → p
// Before:
<div><span>Hello</span></div>
// After:
<p><span>Hello</span></p>
// React destroys div and span, creates new p and spangraph TD
subgraph "Old"
A1[div.old] --> B1[span 'Text']
end
subgraph "New"
A2[div.new] --> B2[span 'Text']
end
A1 -.->|Update Props| A2
B1 -.->|Keep| B2
style A1 fill:#fff4cc
style A2 fill:#ccffcc
Result: Keep the DOM node, update only changed attributes
// Before:
<div className="old" style={{color: 'red'}}>Text</div>
// After:
<div className="new" style={{color: 'blue'}}>Text</div>
// React only updates className and style, keeps the DOM nodeKeys help React identify which items have changed, been added, or removed.
// Initial render:
<ul>
<li>Alice</li>
<li>Bob</li>
</ul>
// After adding Charlie at the beginning:
<ul>
<li>Charlie</li>
<li>Alice</li>
<li>Bob</li>
</ul>What React does: Updates all three <li> elements (inefficient!)
graph LR
subgraph "Before"
A1[li: Alice] --> A2[li: Bob]
end
subgraph "After"
B1[li: Charlie] --> B2[li: Alice] --> B3[li: Bob]
end
A1 -.->|Update to Charlie| B1
A2 -.->|Update to Alice| B2
B3[li: Bob]
style B1 fill:#ffcccc
style B2 fill:#ffcccc
style B3 fill:#ccffcc
// Initial render:
<ul>
<li key="alice">Alice</li>
<li key="bob">Bob</li>
</ul>
// After adding Charlie:
<ul>
<li key="charlie">Charlie</li>
<li key="alice">Alice</li>
<li key="bob">Bob</li>
</ul>What React does: Recognizes Alice and Bob unchanged, only inserts Charlie
graph LR
subgraph "Before"
A1[li key=alice] --> A2[li key=bob]
end
subgraph "After"
B1[li key=charlie] --> B2[li key=alice] --> B3[li key=bob]
end
A1 -.->|Move| B2
A2 -.->|Move| B3
B1[NEW]
style B1 fill:#ccffcc
style B2 fill:#ccccff
style B3 fill:#ccccff
// ✅ Good: Stable, unique keys
items.map(item => <div key={item.id}>{item.name}</div>)
// ❌ Bad: Index as key (unstable when reordering)
items.map((item, index) => <div key={index}>{item.name}</div>)
// ❌ Bad: Random keys (creates new elements every render)
items.map(item => <div key={Math.random()}>{item.name}</div>)Reconciliation is the algorithm React uses to diff one tree with another to determine what needs to change.
graph TD
A[State/Props Change] --> B[Re-render Component]
B --> C[Create New Virtual DOM]
C --> D[Diff with Old Virtual DOM]
D --> E{Changes Found?}
E -->|Yes| F[Create Update Queue]
E -->|No| G[Do Nothing]
F --> H[Batch Updates]
H --> I[Commit to Real DOM]
style A fill:#ffcccc
style C fill:#cce5ff
style D fill:#ffffcc
style I fill:#ccffcc
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}Step 1: Initial Render
graph TD
A[Counter Component] --> B[Virtual DOM]
B --> C[div]
C --> D["p: 'Count: 0'"]
C --> E["button: '+'"]
B --> F[Real DOM]
F --> G[div]
G --> H["p: 'Count: 0'"]
G --> I["button: '+'"]
Step 2: State Update (count = 1)
graph TD
A[setCount Called] --> B[New Virtual DOM]
B --> C[div]
C --> D["p: 'Count: 1'"]
C --> E["button: '+'"]
B --> F[Diff Algorithm]
F --> G{Compare Trees}
G -->|div: Same| H[Keep]
G -->|p text: Changed| I[Update Text Node]
G -->|button: Same| J[Keep]
I --> K[DOM Update]
K --> L[Only update text content of p]
style A fill:#ffcccc
style F fill:#ffffcc
style L fill:#ccffcc
React 16+ uses Fiber architecture with two phases:
graph LR
A[Render Phase] -->|Interruptible| B[Build Work-in-Progress Tree]
B --> C[Diff & Mark Updates]
C --> D[Commit Phase]
D -->|Non-Interruptible| E[Apply DOM Updates]
E --> F[Run Effects]
style A fill:#cce5ff
style D fill:#ccffcc
Fiber is React's reconciliation engine since version 16. It's a reimplementation of the core algorithm.
A Fiber is a JavaScript object representing a unit of work. Each React element has a corresponding Fiber node.
// Simplified Fiber node structure
{
type: 'div', // Component type
key: null, // Key from props
props: { children: [...] }, // Props
stateNode: DOMNode, // Actual DOM node
return: parentFiber, // Parent fiber
child: firstChildFiber, // First child
sibling: nextSiblingFiber, // Next sibling
alternate: oldFiber, // Previous version
effectTag: 'UPDATE', // What needs to be done
nextEffect: nextFiber // Linked list of effects
}graph TD
A[App Fiber] -->|child| B[div Fiber]
B -->|child| C[h1 Fiber]
C -->|sibling| D[p Fiber]
B -->|return| A
C -->|return| B
D -->|return| B
A -.->|alternate| A2[Old App Fiber]
B -.->|alternate| B2[Old div Fiber]
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#ffe1f0
style D fill:#ffe1f0
style A2 fill:#f0f0f0
style B2 fill:#f0f0f0
Fiber enables time-slicing: breaking work into chunks and spreading it across multiple frames.
graph LR
A[Work Loop] --> B{More Work?}
B -->|Yes| C{Time Remaining?}
C -->|Yes| D[Process Next Fiber]
C -->|No| E[Yield to Browser]
E --> A
D --> A
B -->|No| F[Commit Phase]
style E fill:#ffffcc
style F fill:#ccffcc
// Simplified work loop concept
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (nextUnitOfWork) {
// More work to do, schedule next frame
requestIdleCallback(workLoop);
} else {
// All work done, commit to DOM
commitRoot();
}
}Before Fiber (Stack Reconciler):
- Synchronous, recursive
- Once started, couldn't pause
- Long updates blocked the browser
With Fiber:
- Asynchronous, can pause/resume
- Prioritize urgent updates (user input)
- Better perceived performance
graph TD
subgraph "Stack Reconciler"
A1[Start Update] --> B1[Process Entire Tree]
B1 --> C1[Commit]
C1 --> D1[Browser Can Render]
end
subgraph "Fiber Reconciler"
A2[Start Update] --> B2[Process Chunk 1]
B2 --> C2[Yield to Browser]
C2 --> D2[Process Chunk 2]
D2 --> E2[Yield to Browser]
E2 --> F2[Commit]
end
style B1 fill:#ffcccc
style C2 fill:#ccffcc
style E2 fill:#ccffcc
graph TD
A[User Action / setState] --> B[Schedule Update]
B --> C[Render Phase Begins]
C --> D[Component Returns JSX]
D --> E[JSX → React.createElement]
E --> F[Create React Elements]
F --> G[Build Fiber Tree]
G --> H[Diff with Current Tree]
H --> I[Mark Effects UPDATE/DELETE/INSERT]
I --> J{More Work?}
J -->|Yes| K{Has Time?}
K -->|Yes| C
K -->|No| L[Yield to Browser]
L --> C
J -->|No| M[Commit Phase]
M --> N[Apply DOM Mutations]
N --> O[Run useLayoutEffect]
O --> P[Browser Paints]
P --> Q[Run useEffect]
style A fill:#ffcccc
style G fill:#cce5ff
style H fill:#ffffcc
style N fill:#ccffcc
style P fill:#ccffcc
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React' },
{ id: 2, text: 'Build App' }
]);
const addTodo = () => {
setTodos([...todos, { id: 3, text: 'Deploy' }]);
};
return (
<div>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<button onClick={addTodo}>Add</button>
</div>
);
}When addTodo is clicked:
- setState triggers update
- JSX transformed to
React.createElementcalls - Virtual DOM new tree created with 3 todos
- Diffing compares old (2 todos) vs new (3 todos)
- Keys identify that todos 1 & 2 unchanged
- Fiber marks effect: INSERT new
<li>with id=3 - Commit inserts single DOM node
- Result efficient update, only one DOM insertion
1. Use Keys for Lists
// ✅ Efficient reconciliation
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>2. Avoid Inline Functions/Objects in JSX
// ❌ Creates new function every render
<button onClick={() => handleClick(id)}>Click</button>
// ✅ Memoized callback
const handleClickMemoized = useCallback(() => handleClick(id), [id]);
<button onClick={handleClickMemoized}>Click</button>3. Prevent Unnecessary Re-renders
// ✅ Memoize expensive components
const MemoizedChild = React.memo(Child);
// ✅ Use React.memo with custom comparison
const MemoizedItem = React.memo(Item, (prev, next) => {
return prev.id === next.id && prev.text === next.text;
});4. Keep Component Type Stable
// ❌ Creates new component type every render
function Parent() {
const Child = () => <div>Child</div>;
return <Child />;
}
// ✅ Stable component reference
const Child = () => <div>Child</div>;
function Parent() {
return <Child />;
}- JSX is syntactic sugar for
React.createElementcalls - React Elements are plain JavaScript objects describing UI
- Virtual DOM is a lightweight representation enabling efficient updates
- Diffing uses heuristics (type comparison, keys) for O(n) performance
- Fiber enables interruptible rendering and better performance
- Keys are critical for efficient list reconciliation
- Reconciliation compares trees and produces minimal DOM updates
Understanding these internals helps you write more performant React applications and debug issues more effectively.