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

PropsState
OwnerParentSelf
Mutable?NoYes (via setter)
Triggers re-render?When parent passes new valueWhen setter is called

Types of State

TypeTool
LocaluseState, useReducer
Shared (siblings)Lift to parent
GlobalContext, Zustand, Redux
ServerTanStack Query, SWR
URLRouter 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" then setUser(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 (use prev => prev + 1 for sequential updates)

Re-rendering

Three triggers: State change, parent re-render, context change.

Re-render vs Re-mount:

Re-renderRe-mount
StatePreservedReset
DOMReusedDestroyed + recreated
CauseState/parent/context changeKey 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:

ToolPurpose
React.memoSkip re-render if props haven't changed
useMemoStabilize object/computed value references
useCallbackStabilize function references
Move state downReduce blast radius of re-renders
Split contextRe-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

ConceptCore Idea
StateComponent memory that triggers re-renders
useStateReturns [value, setter], initial value used once
SnapshotState is fixed within a single render
ImmutabilityAlways create new references, never mutate
Re-renderFunction runs again; not the same as DOM update
TriggersState change, parent re-render, context change
BatchingMultiple setStates = one re-render (React 18: everywhere)
Functional updatesprev => newValue for sequential/closure-safe updates
Derived stateCompute from state/props, don't store separately
React.memoSkip child re-render when props unchanged
flushSyncForce synchronous render (escape hatch)
FiberInternal architecture enabling interruptible rendering

Navigation: ← Interview Questions · Back to Overview