Episode 2 — React Frontend Architecture NextJS / 2.3 — State and Re rendering Logic
2.3.c — How React Re-renders
In one sentence: Understanding React's re-rendering process — what triggers it, how reconciliation works, and why components re-render when they do — is the key to building performant applications and debugging unexpected behavior.
Navigation: ← 2.3.b — useState Hook · Next → 2.3.d — Batching State Updates
Table of Contents
- What Is a Re-render?
- The Render-Commit Cycle
- What Triggers a Re-render
- The Component Tree and Cascading Renders
- Virtual DOM and Reconciliation
- The Diffing Algorithm in Detail
- When React Skips a Re-render
- React.memo — Preventing Child Re-renders
- Reference Equality and Why It Matters
- Common Re-render Patterns and Anti-Patterns
- Debugging Re-renders with React DevTools
- The Mental Model: State as a Snapshot
- Fiber Architecture — How React Schedules Work
- Concurrent Features and Rendering
- Key Takeaways
- Explain-It Challenge
1. What Is a Re-render?
A re-render is when React calls your component function again to get a new description of what the UI should look like. It's NOT the same as a DOM update.
┌──────────────────────────────────────────────────────────┐
│ RE-RENDER vs DOM UPDATE │
│ │
│ Re-render (cheap): │
│ ┌──────────────┐ │
│ │ React calls │ → Your function runs │
│ │ YourComponent │ → Returns new JSX │
│ │ () │ → Creates new Virtual DOM tree │
│ └──────────────┘ │
│ ↓ │
│ Reconciliation (comparison): │
│ ┌──────────────┐ │
│ │ Diff old │ → Finds minimum changes │
│ │ vs new │ → Calculates what actually changed │
│ │ Virtual DOM │ │
│ └──────────────┘ │
│ ↓ │
│ DOM Update (expensive — only if needed): │
│ ┌──────────────┐ │
│ │ Apply only │ → Only changed nodes touched │
│ │ the diff to │ → Browser repaints only affected area│
│ │ real DOM │ │
│ └──────────────┘ │
│ │
│ Key insight: Many re-renders produce ZERO DOM updates! │
└──────────────────────────────────────────────────────────┘
Why This Distinction Matters
function Counter() {
const [count, setCount] = useState(0);
console.log("Component rendered"); // Fires on EVERY re-render
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
<p>This text never changes</p> {/* Still in the re-render, but no DOM update */}
</div>
);
}
// Click "+1":
// 1. React calls Counter() again (re-render)
// 2. Returns new JSX with count=1
// 3. Diffs: only <h1> text changed from "0" to "1"
// 4. Updates ONE text node in the DOM
// 5. <p> and <button> are untouched in the DOM
2. The Render-Commit Cycle
React processes updates in two distinct phases:
┌─────────────────────────────────────────────────────────────┐
│ RENDER PHASE → COMMIT PHASE │
│ │
│ ┌─────────────────────────────┐ │
│ │ RENDER PHASE │ ← Pure, no side effects │
│ │ │ │
│ │ 1. State/props change │ │
│ │ 2. React calls component() │ │
│ │ 3. Component returns JSX │ │
│ │ 4. React builds new VDOM │ │
│ │ 5. React diffs old vs new │ │
│ │ │ │
│ │ ⚡ Can be interrupted! │ │
│ │ ⚡ Can be discarded! │ │
│ │ ⚡ No DOM mutations! │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ COMMIT PHASE │ ← Mutations happen here │
│ │ │ │
│ │ 6. Apply DOM changes │ │
│ │ 7. Run useLayoutEffect │ │
│ │ 8. Browser paints screen │ │
│ │ 9. Run useEffect │ │
│ │ │ │
│ │ ⚠️ Synchronous │ │
│ │ ⚠️ Cannot be interrupted │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Phase Rules
| Phase | Can Do | Cannot Do |
|---|---|---|
| Render | Return JSX, compute values, read state/props | Mutate DOM, fire API calls, set timers |
| Commit | Update DOM, run effects, measure layout | Skip or interrupt |
This is why components must be "pure" during rendering — no side effects. Side effects belong in useEffect.
3. What Triggers a Re-render
There are exactly three things that trigger a re-render:
Trigger 1: State Change
function App() {
const [count, setCount] = useState(0);
// Clicking this button triggers a re-render
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Trigger 2: Parent Re-renders
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Click</button>
{/* Child re-renders when Parent re-renders, even if its props haven't changed! */}
<Child name="Alice" />
</div>
);
}
function Child({ name }) {
console.log("Child rendered"); // Fires every time Parent re-renders!
return <p>Hello, {name}</p>;
}
This is the #1 source of unnecessary re-renders. The child's props (name="Alice") never change, but it re-renders because its parent re-rendered.
Trigger 3: Context Change
const ThemeContext = createContext("light");
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={theme}>
<Header /> {/* Only re-renders if it consumes ThemeContext */}
<Content />
</ThemeContext.Provider>
);
}
function DeepChild() {
const theme = useContext(ThemeContext);
// Re-renders whenever theme value changes, even if parent doesn't re-render
return <p>Theme: {theme}</p>;
}
What Does NOT Trigger Re-renders
| Action | Triggers Re-render? |
|---|---|
setState(sameValue) | No — React bails out (shallow comparison) |
| Mutating a variable (not state) | No — React doesn't track it |
Changing a ref (ref.current = x) | No — refs don't trigger renders |
props staying the same but parent re-renders | Yes! (unless React.memo) |
| Creating a new object/array that's equal in value | Yes (different reference) |
4. The Component Tree and Cascading Renders
When a component re-renders, all of its descendants re-render too, by default.
┌─────────────────────────────────────────────────────────────┐
│ CASCADE RE-RENDER │
│ │
│ State changes in App: │
│ │
│ ┌──────┐ │
│ │ App │ ← setState() called here │
│ │ 🔄 │ ← Re-renders │
│ └──┬───┘ │
│ ┌────┴──────┐ │
│ ▼ ▼ │
│ ┌──────┐ ┌────────┐ │
│ │Header│ │ Main │ │
│ │ 🔄 │ │ 🔄 │ ← Both re-render (children of App)│
│ └──────┘ └───┬────┘ │
│ ┌───┴────┐ │
│ ▼ ▼ │
│ ┌──────┐ ┌──────┐ │
│ │ List │ │Sidebar│ │
│ │ 🔄 │ │ 🔄 │ ← All descendants re-render │
│ └──┬───┘ └───────┘ │
│ ┌───┴────┐ │
│ ▼ ▼ ▼ │
│ 🔄 🔄 🔄 ← Even deeply nested items │
│ │
│ EVERY component below the state change re-renders! │
│ (Unless stopped by React.memo or useMemo) │
└─────────────────────────────────────────────────────────────┘
Why This Is Usually Fine
React re-renders are cheap. Calling a function and diffing virtual DOM nodes is fast — microseconds per component. The expensive part is DOM mutations, and React minimizes those.
Rules of thumb:
- Don't optimize prematurely — measure first with Profiler
- 100-200 components re-rendering is usually imperceptible (<16ms)
- Optimize only when you see jank, slow interactions, or >16ms render times
5. Virtual DOM and Reconciliation
The Virtual DOM is a lightweight JavaScript representation of the real DOM. React uses it to minimize actual DOM operations.
How It Works
// Your component returns JSX
function App() {
return (
<div className="app">
<h1>Hello</h1>
<p>Count: {count}</p>
</div>
);
}
// React internally creates a tree of objects (Virtual DOM):
{
type: 'div',
props: { className: 'app' },
children: [
{ type: 'h1', props: {}, children: ['Hello'] },
{ type: 'p', props: {}, children: ['Count: ', 5] }
]
}
// On re-render with count=6:
{
type: 'div',
props: { className: 'app' },
children: [
{ type: 'h1', props: {}, children: ['Hello'] }, // Same
{ type: 'p', props: {}, children: ['Count: ', 6] } // Changed!
]
}
// React diffs these two trees and finds:
// Only the text node "5" → "6" changed
// → Updates ONE text node in the real DOM
The Diffing Process
┌─────────────────────────────────────────────────────────┐
│ RECONCILIATION RULES │
│ │
│ Rule 1: Different element types → destroy and rebuild │
│ ────────────────────────────────────────────────────── │
│ Old: <div><Counter /></div> │
│ New: <span><Counter /></span> │
│ → Destroy <div>, destroy Counter (loses state!) │
│ → Create <span>, create new Counter (fresh state) │
│ │
│ Rule 2: Same element type → update attributes only │
│ ────────────────────────────────────────────────────── │
│ Old: <div className="old" style={{color: 'red'}} /> │
│ New: <div className="new" style={{color: 'blue'}} /> │
│ → Update className and style on existing DOM node │
│ → Component keeps its state! │
│ │
│ Rule 3: Same component type → re-render with new props │
│ ────────────────────────────────────────────────────── │
│ Old: <Counter count={5} /> │
│ New: <Counter count={6} /> │
│ → Same Counter instance, update props, re-render │
│ → State is preserved! │
│ │
│ Rule 4: Lists use keys for identity matching │
│ ────────────────────────────────────────────────────── │
│ Old: [key=1, key=2, key=3] │
│ New: [key=2, key=3] │
│ → Remove key=1, keep key=2 and key=3 │
└─────────────────────────────────────────────────────────┘
6. The Diffing Algorithm in Detail
Element Type Comparison
// CASE 1: Same HTML element type — update attributes
// Old:
<div className="before" title="stuff" />
// New:
<div className="after" title="stuff" />
// React: update className from "before" to "after", leave title
// CASE 2: Different HTML element types — tear down and rebuild
// Old:
<div><Counter /></div>
// New:
<article><Counter /></article>
// React: unmount old <div> and Counter, mount new <article> and Counter
// Counter LOSES all state!
// CASE 3: Same component type — update props
// Old:
<UserCard name="Alice" role="admin" />
// New:
<UserCard name="Alice" role="user" />
// React: same UserCard instance, call with new props, state preserved
Children Comparison
// React compares children by POSITION (without keys)
// Old:
<ul>
<li>Alice</li>
<li>Bob</li>
</ul>
// New (prepend Charlie):
<ul>
<li>Charlie</li>
<li>Alice</li>
<li>Bob</li>
</ul>
// Without keys: React compares position-by-position
// Position 0: "Alice" → "Charlie" (update text)
// Position 1: "Bob" → "Alice" (update text)
// Position 2: (new) → "Bob" (insert)
// Result: 3 DOM operations
// With keys: React matches by identity
// key="charlie": new → insert
// key="alice": same → no change
// key="bob": same → no change
// Result: 1 DOM operation
7. When React Skips a Re-render
React has a built-in optimization called bailout: if setState is called with the same value, React skips the re-render.
Same-Value Bailout
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(0); // Same value as current state
// React compares: Object.is(0, 0) === true
// → Skips re-render! Component function doesn't run.
};
console.log("Rendered"); // Does NOT fire when setting same value
return <button onClick={handleClick}>{count}</button>;
}
How React Compares Values
React uses Object.is() for comparison:
// Primitives — compared by value
Object.is(1, 1) // true → skip re-render
Object.is("hello", "hello") // true → skip
Object.is(true, true) // true → skip
// Objects — compared by REFERENCE
Object.is({a: 1}, {a: 1}) // false! Different objects → RE-RENDER
Object.is([1,2], [1,2]) // false! Different arrays → RE-RENDER
const obj = {a: 1};
Object.is(obj, obj) // true → skip (same reference)
This is why you should never create new objects/arrays in state updates unless the data actually changed:
// ❌ Always re-renders — new object every time
setUser({ ...user }); // Even if nothing changed, new reference!
// ✅ Only re-renders when data changes
setUser(prev => {
if (prev.name === newName) return prev; // Same reference → skip
return { ...prev, name: newName }; // New reference → re-render
});
8. React.memo — Preventing Child Re-renders
React.memo wraps a component and prevents re-rendering when props haven't changed:
// Without memo — re-renders every time Parent re-renders
function ExpensiveList({ items }) {
console.log("ExpensiveList rendered");
return items.map(item => <div key={item.id}>{item.name}</div>);
}
// With memo — only re-renders when `items` prop actually changes
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
console.log("ExpensiveList rendered");
return items.map(item => <div key={item.id}>{item.name}</div>);
});
How memo Works
┌─────────────────────────────────────────────────────────┐
│ React.memo FLOW │
│ │
│ Parent re-renders │
│ │ │
│ ▼ │
│ React prepares to render <MemoizedChild props={...}> │
│ │ │
│ ▼ │
│ Compare previous props vs new props │
│ (shallow equality: Object.is for each prop) │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ Same Different │
│ │ │ │
│ ▼ ▼ │
│ SKIP RE-RENDER │
│ (reuse (call component │
│ previous function again) │
│ output) │
└─────────────────────────────────────────────────────────┘
When memo Helps vs. Hurts
| Scenario | memo Helpful? | Why |
|---|---|---|
| Expensive computation in child | Yes | Avoids costly recalculation |
| Large list of items | Yes | Prevents re-rendering 100+ children |
| Child has same props always | Yes | Prevents unnecessary work |
| Props change on every render | No | memo comparison cost + re-render anyway |
| Cheap component (few elements) | No | Overhead of comparison > benefit |
| Props include inline functions/objects | No | New reference every time → always re-renders |
9. Reference Equality and Why It Matters
This is the most common source of "why is my memoized component still re-rendering?"
The Problem
function Parent() {
const [count, setCount] = useState(0);
// ❌ New object created on EVERY render
const style = { color: "red", fontSize: 16 };
// ❌ New array created on EVERY render
const items = [1, 2, 3];
// ❌ New function created on EVERY render
const handleClick = () => console.log("clicked");
return (
<>
<button onClick={() => setCount(c => c + 1)}>Re-render</button>
{/* MemoizedChild STILL re-renders because props are new references! */}
<MemoizedChild style={style} items={items} onClick={handleClick} />
</>
);
}
The Solution: useMemo and useCallback
function Parent() {
const [count, setCount] = useState(0);
// ✅ Same reference unless dependencies change
const style = useMemo(() => ({ color: "red", fontSize: 16 }), []);
const items = useMemo(() => [1, 2, 3], []);
const handleClick = useCallback(() => console.log("clicked"), []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Re-render</button>
{/* Now MemoizedChild skips re-render — props references are stable */}
<MemoizedChild style={style} items={items} onClick={handleClick} />
</>
);
}
Reference Equality Cheat Sheet
// PRIMITIVES — compared by value (always stable)
"hello" === "hello" // true ✅
42 === 42 // true ✅
true === true // true ✅
// OBJECTS/ARRAYS/FUNCTIONS — compared by reference
{} === {} // false ❌ (different objects)
[] === [] // false ❌ (different arrays)
(() => {}) === (() => {}) // false ❌ (different functions)
// Same reference — stable
const obj = {};
obj === obj // true ✅
10. Common Re-render Patterns and Anti-Patterns
Anti-Pattern 1: State in the Wrong Place
// ❌ Typing in SearchBar re-renders the ENTIRE product list
function App() {
const [query, setQuery] = useState("");
const [products, setProducts] = useState([...]);
return (
<div>
<SearchBar query={query} onChange={setQuery} />
<ProductList products={products} /> {/* Re-renders on every keystroke! */}
</div>
);
}
// ✅ Move search state down — only SearchBar re-renders
function App() {
const [products, setProducts] = useState([...]);
return (
<div>
<SearchBar /> {/* Manages its own query state */}
<ProductList products={products} /> {/* Only re-renders when products change */}
</div>
);
}
Anti-Pattern 2: Inline Object/Function Props
// ❌ New object/function on every render
<UserCard style={{ margin: 10 }} onEdit={() => handleEdit(user.id)} />
// ✅ Stable references
const cardStyle = useMemo(() => ({ margin: 10 }), []);
const handleEdit = useCallback(() => editUser(user.id), [user.id]);
<UserCard style={cardStyle} onEdit={handleEdit} />
Pattern: Composition to Avoid Re-renders
// ❌ ExpensiveTree re-renders when color changes
function App() {
const [color, setColor] = useState("red");
return (
<div style={{ color }}>
<input value={color} onChange={e => setColor(e.target.value)} />
<ExpensiveTree /> {/* Re-renders on every keystroke! */}
</div>
);
}
// ✅ Move state down — ExpensiveTree doesn't re-render
function App() {
return (
<div>
<ColorPicker /> {/* Color state lives here */}
<ExpensiveTree /> {/* Never re-renders for color changes */}
</div>
);
}
function ColorPicker() {
const [color, setColor] = useState("red");
return (
<div style={{ color }}>
<input value={color} onChange={e => setColor(e.target.value)} />
</div>
);
}
Pattern: Children as Props (Slot Pattern)
// ✅ Pass expensive component as children — it's created by the parent
// and doesn't re-render when ColorPicker's state changes
function App() {
return (
<ColorPicker>
<ExpensiveTree /> {/* Created by App, not by ColorPicker */}
</ColorPicker>
);
}
function ColorPicker({ children }) {
const [color, setColor] = useState("red");
return (
<div style={{ color }}>
<input value={color} onChange={e => setColor(e.target.value)} />
{children} {/* Same JSX element reference — no re-render! */}
</div>
);
}
11. Debugging Re-renders with React DevTools
Using the Profiler
┌─────────────────────────────────────────────────────────┐
│ React DevTools → Profiler Tab │
│ │
│ 1. Click Record button │
│ 2. Interact with your app │
│ 3. Click Stop │
│ 4. Analyze the flame graph: │
│ │
│ ┌─ App (2.1ms) ────────────────────────┐ │
│ │ ┌─ Header (0.1ms) ──┐ ┌─ Main ─────┐│ │
│ │ │ ┌ Logo (0.05ms) ┐ │ │ ┌ List ───┐││ │
│ │ │ └────────────────┘ │ │ │ Item ×50│││ │
│ │ └────────────────────┘ │ └─────────┘││ │
│ │ └────────────┘│ │
│ └──────────────────────────────────────┘ │
│ │
│ Colors: │
│ 🟩 Green = fast render (<1ms) │
│ 🟨 Yellow = medium render (1-16ms) │
│ 🟥 Red = slow render (>16ms) │
│ ⬜ Gray = did not render │
│ │
│ "Why did this render?" shows: │
│ - Props changed │
│ - State changed │
│ - Parent re-rendered │
│ - Context changed │
└─────────────────────────────────────────────────────────┘
Highlight Updates Setting
In React DevTools → Settings → enable "Highlight updates when components render". Components flash with colored borders when they re-render:
- Blue: infrequent renders
- Green: moderate frequency
- Yellow/Red: frequent renders (potential problem)
Console Logging Pattern
function MyComponent(props) {
// Debug: log every render with props
console.log("MyComponent rendered", { props });
// Debug: track render count
const renderCount = useRef(0);
renderCount.current++;
console.log(`MyComponent render #${renderCount.current}`);
return <div>...</div>;
}
12. The Mental Model: State as a Snapshot
Each render is a "snapshot" — a frozen picture of the UI at one point in time. When state changes, React takes a new snapshot.
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// All three setCount calls see count = 0 (the snapshot value)
setCount(count + 1); // Sets to 1
setCount(count + 1); // Sets to 1 (still reads count=0!)
setCount(count + 1); // Sets to 1 (still reads count=0!)
};
// Result: count goes from 0 → 1, not 0 → 3
return <button onClick={handleClick}>{count}</button>;
}
// Fix: Use the updater function to get the LATEST value
const handleClick = () => {
setCount(prev => prev + 1); // 0 → 1
setCount(prev => prev + 1); // 1 → 2
setCount(prev => prev + 1); // 2 → 3
};
// Result: count goes from 0 → 3 ✅
The Snapshot Analogy
┌─────────────────────────────────────────────────────────┐
│ STATE AS SNAPSHOTS │
│ │
│ Render 1 (snapshot): Render 2 (snapshot): │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ count = 0 │ │ count = 1 │ │
│ │ text = "Hello" │ │ text = "Hello" │ │
│ │ items = [a, b] │ │ items = [a, b, c] │ │
│ │ │ │ │ │
│ │ Every variable, │ │ New snapshot with │ │
│ │ every function, │ │ updated state. │ │
│ │ every event handler│ │ Old snapshot is │ │
│ │ sees THIS version │ │ gone. │ │
│ └────────────────────┘ └────────────────────┘ │
│ │
│ State updates don't change the current snapshot. │
│ They schedule a NEW render with a NEW snapshot. │
└─────────────────────────────────────────────────────────┘
Closures Capture the Snapshot
function Timer() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
// This alert captures count from the snapshot when handleClick was created
alert(`Count was: ${count}`);
}, 3000);
};
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={handleClick}>Alert in 3s</button>
</div>
);
}
// Steps:
// 1. count = 0
// 2. Click "Alert in 3s" → captures count=0 in closure
// 3. Click "+1" three times → count becomes 3
// 4. Alert fires → shows "Count was: 0" (not 3!)
// Because the setTimeout closure captured the count=0 snapshot
13. Fiber Architecture — How React Schedules Work
React's internal engine (since React 16) is called Fiber. It enables interruptible rendering.
┌─────────────────────────────────────────────────────────┐
│ FIBER ARCHITECTURE │
│ │
│ Each component instance is a "Fiber node": │
│ │
│ Fiber { │
│ type: Component, // What to render │
│ props: { ... }, // Input data │
│ state: { ... }, // Internal data │
│ child: Fiber | null, // First child │
│ sibling: Fiber | null, // Next sibling │
│ return: Fiber | null, // Parent │
│ effectTag: string, // What DOM operation needed │
│ alternate: Fiber, // Previous version (double │
│ } // buffering) │
│ │
│ React walks the Fiber tree: │
│ │
│ App │
│ ╱ ╲ │
│ Header Main ← Walk: App → Header → Logo → │
│ │ │ (back up) → Main → Sidebar → │
│ Logo Sidebar Content → (done) │
│ │ │
│ Content │
│ │
│ Key feature: React can PAUSE this walk at any │
│ fiber node and resume later (time-slicing). │
└─────────────────────────────────────────────────────────┘
Why Fiber Matters
Before Fiber (React 15): Rendering was synchronous. A large update would block the main thread, causing janky animations and unresponsive inputs.
With Fiber: React breaks rendering into small units of work (one fiber at a time) and can:
- Pause rendering to handle high-priority events (user typing, animations)
- Resume rendering where it left off
- Abort rendering if a newer update makes it obsolete
- Reuse previous work when possible
14. Concurrent Features and Rendering
React 18+ introduced concurrent rendering features that take advantage of Fiber:
Transitions (useTransition)
function SearchPage() {
const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// High priority — update input immediately
setQuery(e.target.value);
// Low priority — can be interrupted
startTransition(() => {
setFilteredResults(filterHugeList(e.target.value));
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : <ResultsList results={filteredResults} />}
</div>
);
}
Suspense
// Declarative loading states
function ProfilePage({ userId }) {
return (
<Suspense fallback={<ProfileSkeleton />}>
<ProfileDetails userId={userId} />
<Suspense fallback={<PostsSkeleton />}>
<ProfilePosts userId={userId} />
</Suspense>
</Suspense>
);
}
Summary of Concurrent Features
| Feature | Purpose |
|---|---|
useTransition | Mark non-urgent updates as low priority |
useDeferredValue | Defer re-rendering with a stale value |
Suspense | Declarative loading states for async operations |
startTransition | Non-hook version for class components / outside React |
15. Key Takeaways
- Re-render ≠ DOM update — React calls your function, diffs the result, and only updates what actually changed in the DOM
- Three triggers: state change, parent re-render, context change
- Cascading: when a component re-renders, ALL descendants re-render by default
- Reconciliation: React diffs old vs new Virtual DOM trees and applies minimal DOM changes
- Same type = update, different type = destroy and rebuild — this is why component position matters
- Object.is() for comparison — same primitive value skips re-render, but
{} !== {} - React.memo prevents child re-renders when props haven't changed (shallow comparison)
- Reference equality is critical — inline objects/arrays/functions create new references every render
- State is a snapshot — each render captures state at that moment; closures keep the snapshot
- Don't optimize prematurely — measure with Profiler first, then apply memo/useMemo/useCallback where it matters
Explain-It Challenge
-
The Photo Lab: Explain React's render-commit cycle using a photo development analogy. The render phase is like developing a negative (can be discarded), the commit phase is like printing the photo (permanent). What happens when you develop a new negative before the old one is printed?
-
The Falling Dominoes: When a parent component re-renders, all children re-render too — like dominoes falling. Explain three ways to "block" a domino from falling (React.memo, composition, moving state down). What are the tradeoffs of each approach?
-
The Detective: Your colleague says "my component renders 50 times per second when I type in the search bar." Walk them through a debugging process: what tools would you use, what questions would you ask, and what are the three most likely causes?
Navigation: ← 2.3.b — useState Hook · Next → 2.3.d — Batching State Updates