Episode 2 — React Frontend Architecture NextJS / 2.3 — State and Rerendering Logic
2.3 — Quick Revision
Compact cheat sheet for State and Re-rendering Logic. Print this, pin it, revisit it.
Navigation: ← Interview Questions · Back to Overview
What Is State
State = component memory that persists between renders and triggers re-renders on change.
- Regular variables reset on each render and don't trigger re-renders.
- State is stored in React's internal memory (Fiber nodes), not in the function scope.
- Each component instance has its own isolated state.
- State is a snapshot: within one render, the value is fixed.
Props vs State
| Props | State | |
|---|---|---|
| Owner | Parent | Self |
| Mutable? | No | Yes (via setter) |
| Triggers re-render? | When parent passes new value | When setter is called |
Types of State
| Type | Tool |
|---|---|
| Local | useState, useReducer |
| Shared (siblings) | Lift to parent |
| Global | Context, Zustand, Redux |
| Server | TanStack Query, SWR |
| URL | Router params |
useState Hook
const [value, setValue] = useState(initialValue);
Rules:
- Call at top level only (no conditions, no loops)
- Initial value used on first render only
- Setter triggers re-render with new value
- State updates are asynchronous (new value on next render)
Updating:
setValue(newValue); // Direct (for independent values)
setValue(prev => prev + 1); // Functional (when depending on previous)
Lazy initialization (expensive initial values):
useState(() => expensiveComputation()); // Function called only on first render
Immutable updates:
// Objects
setUser({ ...user, name: "Bob" });
// Arrays
setItems([...items, newItem]); // Add
setItems(items.filter(i => i.id !== id)); // Remove
setItems(items.map(i => i.id === id ? { ...i, done: true } : i)); // Update
setItems([...items].sort(compareFn)); // Sort (copy first!)
Common mistakes:
- Mutating state directly (
user.name = "Bob"thensetUser(user)-- won't re-render) - Reading state right after setting it (still shows old snapshot)
- Setting state during render (infinite loop)
setCount(count + 1)x3 = +1, not +3 (useprev => prev + 1for sequential updates)
Re-rendering
Three triggers: State change, parent re-render, context change.
Re-render vs Re-mount:
| Re-render | Re-mount | |
|---|---|---|
| State | Preserved | Reset |
| DOM | Reused | Destroyed + recreated |
| Cause | State/parent/context change | Key change, type change |
Two phases:
Render Phase (pure, interruptible):
Component function runs --> JSX produced --> Diff computed
Commit Phase (synchronous):
DOM updated --> Refs updated --> Effects scheduled
Rendering is NOT painting. Many re-renders produce zero DOM changes.
Preventing unnecessary re-renders:
| Tool | Purpose |
|---|---|
React.memo | Skip re-render if props haven't changed |
useMemo | Stabilize object/computed value references |
useCallback | Stabilize function references |
| Move state down | Reduce blast radius of re-renders |
| Split context | Re-render only relevant consumers |
Optimize only when measured. Most re-renders are fast and harmless.
Batching
Multiple setState calls in one synchronous block = ONE re-render.
React 18: Automatic batching everywhere (event handlers, setTimeout, promises, native events).
Requires: createRoot API.
function handler() {
setA(1); // queued
setB(2); // queued
setC(3); // queued
} // ONE re-render with all three values
Async functions: Each synchronous block between await is a separate batch.
async function submit() {
setLoading(true); setError(null); // Batch 1
const data = await fetch(...);
setData(data); setLoading(false); // Batch 2
}
flushSync: Forces immediate render (escape hatch, use rarely).
import { flushSync } from "react-dom";
flushSync(() => setCount(1)); // DOM updated before next line
Direct vs Functional in a batch:
setCount(1); setCount(2); setCount(3); // Result: 3 (last value wins)
setCount(p => p+1); setCount(p => p+1); setCount(p => p+1); // Result: +3 (all applied)
Derived State
If you can compute it from existing state/props, DON'T store it in state.
Anti-pattern:
const [items, setItems] = useState([...]);
const [count, setCount] = useState(0);
useEffect(() => setCount(items.length), [items]); // BAD: extra render, sync risk
Correct:
const [items, setItems] = useState([...]);
const count = items.length; // GOOD: always correct, no sync needed
Common derived values:
- Filtered/sorted lists from source list + filter/sort criteria
- Counts, sums, averages from arrays
- Validation results from form fields
- Display strings from raw data
- Boolean flags (
isEmpty,isValid,hasError)
Use useMemo only when:
- Computation is measurably expensive (>1ms)
- Dependencies change infrequently relative to re-renders
The test: If you always update X alongside Y, X is probably derived from Y.
Decision Quick Reference
Is it constant? --> const THING = value
From parent via props? --> Use the prop directly
Computable from state/props? --> const derived = compute(state)
Expensive to compute? --> useMemo(() => compute(state), [state])
Changes over time, not computable? --> useState (actual state!)
Changes but doesn't affect UI? --> useRef
From URL? --> Router params
From API? --> Server state library
Key Patterns
Controlled input:
const [value, setValue] = useState("");
<input value={value} onChange={e => setValue(e.target.value)} />
Toggle:
const [isOpen, setIsOpen] = useState(false);
<button onClick={() => setIsOpen(prev => !prev)}>Toggle</button>
List operations:
// Add
setItems(prev => [...prev, newItem]);
// Remove
setItems(prev => prev.filter(i => i.id !== id));
// Update
setItems(prev => prev.map(i => i.id === id ? { ...i, done: true } : i));
Status instead of multiple booleans:
const [status, setStatus] = useState("idle"); // "idle" | "loading" | "error" | "success"
Dynamic object field update:
function handleChange(e) {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
}
One-Page Summary
| Concept | Core Idea |
|---|---|
| State | Component memory that triggers re-renders |
| useState | Returns [value, setter], initial value used once |
| Snapshot | State is fixed within a single render |
| Immutability | Always create new references, never mutate |
| Re-render | Function runs again; not the same as DOM update |
| Triggers | State change, parent re-render, context change |
| Batching | Multiple setStates = one re-render (React 18: everywhere) |
| Functional updates | prev => newValue for sequential/closure-safe updates |
| Derived state | Compute from state/props, don't store separately |
| React.memo | Skip child re-render when props unchanged |
| flushSync | Force synchronous render (escape hatch) |
| Fiber | Internal architecture enabling interruptible rendering |
Navigation: ← Interview Questions · Back to Overview