Virtual DOM & Reconciliation Algorithm

šŸ“– Concept

The Virtual DOM is React's in-memory representation of the UI. It's a lightweight JavaScript object tree that mirrors the actual DOM (web) or native view hierarchy (React Native). When state changes, React creates a NEW virtual tree, diffs it against the previous one, and applies minimal updates.

Why this matters for React Native: In RN, the "DOM" is the native view hierarchy (UIView on iOS, android.view.View on Android). Reconciliation determines which native views to create, update, or destroy — each operation crosses the JS-Native bridge, so minimizing them is critical for performance.

Reconciliation Algorithm (Diffing): React uses a heuristic O(n) algorithm instead of the optimal O(n³) tree diff:

  1. Different types → full replace: If a <View> becomes a <ScrollView>, React destroys the entire subtree and rebuilds. No attempt to match children.

  2. Same type → update props: If a <Text style={A}> becomes <Text style={B}>, React updates only the changed props. No remount.

  3. Lists → key-based matching: For sibling elements, React uses key to match old and new elements. Without keys, React matches by index — leading to bugs and unnecessary remounts.

The key prop deep dive:

  • Keys must be stable, unique, and predictable across renders
  • Using array index as key is wrong when items can be reordered, inserted, or deleted
  • Keys should be domain identifiers (database IDs), NOT random values (Math.random()) which defeat the purpose
  • Key changes force unmount/remount — useful for resetting component state

What reconciliation CAN'T optimize:

  • Moving a component to a different parent still causes unmount/remount
  • Changing component type (even if structure is identical) causes full teardown
  • Deep tree changes propagate even if only a leaf changed — this is where memoization helps

šŸ’» Code Example

codeTap to expand ā›¶
1// === RECONCILIATION IN ACTION ===
2
3// Scenario 1: Element type change → full teardown
4// Before:
5<View style={styles.container}>
6 <TextInput value={text} />
7</View>
8
9// After: Changed View to ScrollView
10<ScrollView style={styles.container}>
11 <TextInput value={text} /> {/* TextInput is REMOUNTED — loses focus, state */}
12</ScrollView>
13// React destroys entire <View> subtree and creates new <ScrollView> subtree
14
15// Scenario 2: Same type → prop update (efficient)
16// Before:
17<Text style={{ color: 'red' }}>Hello</Text>
18// After:
19<Text style={{ color: 'blue' }}>Hello</Text>
20// React only updates the color prop on the existing native TextView
21
22// Scenario 3: Key-based reconciliation
23// āŒ BAD: Using index as key with reorderable list
24function TaskList({ tasks }) {
25 return tasks.map((task, index) => (
26 <TaskItem key={index} task={task} />
27 // If tasks are reordered, index stays the same but task changes
28 // React sees same key → updates props instead of moving
29 // Internal state (checkbox, text input) stays with the wrong item!
30 ));
31}
32
33// āœ… GOOD: Using stable IDs as keys
34function TaskList({ tasks }) {
35 return tasks.map((task) => (
36 <TaskItem key={task.id} task={task} />
37 // If tasks are reordered, React matches by ID
38 // Moves the native views instead of updating wrong ones
39 ));
40}
41
42// Scenario 4: Key change to force remount (intentional reset)
43function ProfileScreen({ userId }) {
44 // When userId changes, we want to completely reset the form
45 return <ProfileForm key={userId} userId={userId} />;
46 // Key change → old ProfileForm unmounts, new one mounts with fresh state
47}
48
49// === UNDERSTANDING WHAT TRIGGERS RECONCILIATION ===
50
51// Every setState/dispatch/context change triggers:
52// 1. Component re-renders (function called again)
53// 2. New virtual tree created for that subtree
54// 3. Diffed against previous virtual tree
55// 4. Minimal native view updates sent across bridge
56
57// This is WHY re-renders themselves aren't expensive —
58// the RECONCILIATION output (bridge calls) is expensive.
59
60// Proving it: component renders 1000 times but if output is same,
61// zero native updates occur
62function ExpensiveRender() {
63 const [, forceRender] = useState(0);
64
65 // This re-renders but produces identical virtual tree
66 // React's reconciliation finds zero diffs → zero bridge calls
67 useEffect(() => {
68 const id = setInterval(() => forceRender(n => n + 1), 10);
69 return () => clearInterval(id);
70 }, []);
71
72 return <Text>Static content</Text>; // Same every time
73}

šŸ‹ļø Practice Exercise

Reconciliation Deep Dive:

  1. Create a list of 100 items, add an item at the beginning — measure performance with index keys vs ID keys
  2. Build a component that conditionally renders <View> or <ScrollView> and observe the remount behavior using useEffect cleanup logs
  3. Implement a key-change based animation by forcing remount of a component with a new key
  4. Profile reconciliation using React DevTools Profiler — identify which components re-render unnecessarily
  5. Create a scenario where moving a component to a different parent causes visible state loss

āš ļø Common Mistakes

  • Using array index as key for dynamic lists — causes incorrect state association when items are reordered or deleted

  • Using Math.random() or Date.now() as key — creates new key every render, forcing full remount every time

  • Assuming re-renders are expensive — re-renders (calling the function) are cheap, it's the reconciliation OUTPUT (bridge calls) that's expensive

  • Wrapping everything in React.memo to prevent re-renders — this adds comparison overhead that can be worse than the re-render itself for simple components

  • Not understanding that parent re-render triggers child re-render even if child props haven't changed — this is by design, use memo strategically

šŸ’¼ Interview Questions

šŸŽ¤ Mock Interview

Mock interview is powered by AI for Virtual DOM & Reconciliation Algorithm. Login to unlock this feature.