React Deep Dive
Chapter 3: Fiber's Reconciliation Process
In the previous chapter, we learned how Fiber breaks rendering work into small units. Now, let's explore how Fiber handles updates and reconciliation. The key to understanding this process is the concept of the workInProgress tree.
The Two Trees
Fiber maintains two trees during the reconciliation process:
- Current Tree: The tree that's currently rendered on the screen
- WorkInProgress Tree: The tree being built for the next render
1// Simplified representation of the two trees
2let currentTree = null; // The current tree
3let workInProgressTree = null; // The tree being built
4
5function createWorkInProgress(current, pendingProps) {
6 let workInProgress = current.alternate;
7 if (workInProgress === null) {
8 // Create a new fiber if we don't have one
9 workInProgress = createFiber(
10 current.type,
11 pendingProps,
12 current.key
13 );
14 workInProgress.alternate = current;
15 current.alternate = workInProgress;
16 } else {
17 // Reuse the existing fiber
18 workInProgress.pendingProps = pendingProps;
19 workInProgress.effectTag = NoEffect;
20 workInProgress.nextEffect = null;
21 workInProgress.firstEffect = null;
22 workInProgress.lastEffect = null;
23 }
24 return workInProgress;
25}
The Reconciliation Process
The reconciliation process follows these steps:
- Begin Work: Start building the workInProgress tree
- Complete Work: Finish processing a fiber node
- Commit Work: Apply changes to the DOM
Here's how it works:
1function beginWork(current, workInProgress, renderLanes) {
2 // 1. Check if we can reuse the current fiber
3 if (current !== null) {
4 const oldProps = current.memoizedProps;
5 const newProps = workInProgress.pendingProps;
6
7 if (oldProps !== newProps) {
8 // Props changed, mark for update
9 workInProgress.effectTag |= Update;
10 }
11 }
12
13 // 2. Process the fiber based on its type
14 switch (workInProgress.tag) {
15 case FunctionComponent:
16 return updateFunctionComponent(current, workInProgress);
17 case HostComponent:
18 return updateHostComponent(current, workInProgress);
19 // ... other cases
20 }
21}
22
23function completeWork(current, workInProgress) {
24 const newProps = workInProgress.pendingProps;
25
26 switch (workInProgress.tag) {
27 case HostComponent: {
28 // Create or update DOM node
29 if (current !== null && workInProgress.stateNode != null) {
30 // Update existing node
31 updateHostComponent(current, workInProgress);
32 } else {
33 // Create new node
34 const instance = createInstance(workInProgress.type, newProps);
35 workInProgress.stateNode = instance;
36 }
37 break;
38 }
39 // ... other cases
40 }
41}
Effect List
Fiber maintains an effect list to track which nodes need updates:
1function completeUnitOfWork(unitOfWork) {
2 let completedWork = unitOfWork;
3 do {
4 const current = completedWork.alternate;
5 const returnFiber = completedWork.return;
6
7 // Append effects to the effect list
8 if (returnFiber !== null && (completedWork.effectTag & Incomplete) === NoEffect) {
9 if (returnFiber.firstEffect === null) {
10 returnFiber.firstEffect = completedWork.firstEffect;
11 }
12 if (completedWork.lastEffect !== null) {
13 if (returnFiber.lastEffect !== null) {
14 returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
15 }
16 returnFiber.lastEffect = completedWork.lastEffect;
17 }
18 }
19 } while (completedWork !== null);
20}
The Commit Phase
After the workInProgress tree is complete, React enters the commit phase:
1function commitRoot(root) {
2 const finishedWork = root.finishedWork;
3
4 // 1. Commit all the side effects
5 commitMutationEffects(finishedWork);
6
7 // 2. Switch the current and workInProgress trees
8 root.current = finishedWork;
9
10 // 3. Commit layout effects
11 commitLayoutEffects(finishedWork);
12}
This is a simplified view of the commit phase. In Chapter 5, we'll dive deeper into:
- How React handles different types of effects
- The three sub-phases of the commit phase
- How React ensures consistency during commits
- The role of layout effects and their timing
Benefits of This Approach
- Incremental Updates: Changes can be applied incrementally without blocking the main thread
- Consistent UI: The current tree remains stable while the workInProgress tree is being built
- Optimized Performance: Only necessary updates are applied to the DOM
- Better Error Handling: If an error occurs, we can fall back to the current tree
Example: Updating a Component
Let's see how this works in practice with a state update:
1function Counter() {
2 const [count, setCount] = useState(0);
3
4 return (
5 <div>
6 <p>Count: {count}</p>
7 <button onClick={() => setCount(count + 1)}>
8 Increment
9 </button>
10 </div>
11 );
12}
When the button is clicked, here's what happens:
-
State Update Triggered
- setCount is called, triggering a state update
- React schedules a re-render for the component
-
WorkInProgress Tree Creation
- React creates a new workInProgress tree
- The tree is built incrementally, one fiber at a time
-
Reconciliation Process
- The Counter fiber is marked for update
- The text node containing the count is marked for update
- Other nodes (div, button, etc.) are reused without changes
-
Commit Phase
- The new count value is committed to the DOM
- The workInProgress tree becomes the current tree
- The UI reflects the updated count
This example shows how Fiber:
- Handles state updates efficiently by only updating what changed
- Maintains consistency between the current and workInProgress trees
- Optimizes performance by reusing unchanged parts of the tree
- Ensures smooth updates without blocking the main thread
In the next chapter, we'll explore how Fiber handles different types of updates and priorities.