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:
Different types ā full replace: If a
<View>becomes a<ScrollView>, React destroys the entire subtree and rebuilds. No attempt to match children.Same type ā update props: If a
<Text style={A}>becomes<Text style={B}>, React updates only the changed props. No remount.Lists ā key-based matching: For sibling elements, React uses
keyto 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
1// === RECONCILIATION IN ACTION ===23// Scenario 1: Element type change ā full teardown4// Before:5<View style={styles.container}>6 <TextInput value={text} />7</View>89// After: Changed View to ScrollView10<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> subtree1415// 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 TextView2122// Scenario 3: Key-based reconciliation23// ā BAD: Using index as key with reorderable list24function 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 changes28 // React sees same key ā updates props instead of moving29 // Internal state (checkbox, text input) stays with the wrong item!30 ));31}3233// ā GOOD: Using stable IDs as keys34function TaskList({ tasks }) {35 return tasks.map((task) => (36 <TaskItem key={task.id} task={task} />37 // If tasks are reordered, React matches by ID38 // Moves the native views instead of updating wrong ones39 ));40}4142// Scenario 4: Key change to force remount (intentional reset)43function ProfileScreen({ userId }) {44 // When userId changes, we want to completely reset the form45 return <ProfileForm key={userId} userId={userId} />;46 // Key change ā old ProfileForm unmounts, new one mounts with fresh state47}4849// === UNDERSTANDING WHAT TRIGGERS RECONCILIATION ===5051// Every setState/dispatch/context change triggers:52// 1. Component re-renders (function called again)53// 2. New virtual tree created for that subtree54// 3. Diffed against previous virtual tree55// 4. Minimal native view updates sent across bridge5657// This is WHY re-renders themselves aren't expensive ā58// the RECONCILIATION output (bridge calls) is expensive.5960// Proving it: component renders 1000 times but if output is same,61// zero native updates occur62function ExpensiveRender() {63 const [, forceRender] = useState(0);6465 // This re-renders but produces identical virtual tree66 // React's reconciliation finds zero diffs ā zero bridge calls67 useEffect(() => {68 const id = setInterval(() => forceRender(n => n + 1), 10);69 return () => clearInterval(id);70 }, []);7172 return <Text>Static content</Text>; // Same every time73}
šļø Practice Exercise
Reconciliation Deep Dive:
- Create a list of 100 items, add an item at the beginning ā measure performance with index keys vs ID keys
- Build a component that conditionally renders
<View>or<ScrollView>and observe the remount behavior using useEffect cleanup logs - Implement a key-change based animation by forcing remount of a component with a new key
- Profile reconciliation using React DevTools Profiler ā identify which components re-render unnecessarily
- 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.