React Patterns & State Management
Controlled vs. Uncontrolled Components
// Controlled — React state IS the source of truth for the value
const [val, setVal] = useState('');
<input value={val} onChange={e => setVal(e.target.value)} />
// Uncontrolled — the DOM itself owns the value; React reads it only when needed
const inputRef = useRef();
<input ref={inputRef} defaultValue="initial" />
inputRef.current.value; // read on demand (e.g., on submit)
Controlled is the default choice when validation or reactive UI (character counts, conditional formatting) depends on the current value on every keystroke. Uncontrolled is the right choice for performance-sensitive forms with many fields where re-rendering on every keystroke is wasteful and nothing else in the UI needs to react to interim values.
Lifting State Up
When two sibling components need to share state, move it to their closest common ancestor and pass it down via props — the standard first-reach-for pattern before introducing Context or a state library.
Composition Over Inheritance
// children prop — the most common composition primitive
function Card({ children }) { return <div className="card">{children}</div>; }
// render props — pass behavior, not just data, as a prop
function Toggle({ render }) {
const [on, setOn] = useState(false);
return render(on, () => setOn(o => !o));
}
React deliberately favors composition (wrapping components inside other components) over class-based inheritance hierarchies — the same "favor composition" principle behind the Strategy/Decorator patterns in [[OOD Design Patterns]], applied to component design instead of class design.
Custom Hooks
Extract reusable stateful logic into a plain function starting with use — see the useFetch example in [[React Hooks Deep-Dive]]. The extraction boundary is exactly the same reasoning as [[SOLID Principles]]'s Single Responsibility Principle: a component's render logic and its data-fetching logic are two different concerns, cleanly separable into a component and a custom hook.
Prop Drilling & Its Fixes
Passing a prop through several layers of components that don't themselves use it, purely to reach a deeply nested consumer, is prop drilling. Fixes, in order of complexity: lift state + pass down (fine for 2-3 levels), Context (simple global-ish values — theme, current user, feature flags), a dedicated state library (Zustand/Redux — when updates are frequent, state is complex, or many unrelated components need independent slices of it).
State Management Decision Tree
Is the state UI-only (modal open, active tab)?
→ useState or useReducer, kept local to the component that owns it
Is the state needed by 2-3 nearby components?
→ lift state up to their closest common ancestor
Is the state needed across most of the app?
→ React Context (simple, infrequent updates) or Zustand/Redux (complex, frequent updates)
Is the state actually server data (fetched, cached, can go stale)?
→ React Query / TanStack Query — handles caching, loading/error state, and refetching,
which is a genuinely different problem from client-only UI state
Why server state gets its own answer, not just "put it in Context": server data has concerns client state doesn't — staleness, refetch-on-focus, request deduplication, background revalidation — a dedicated data-fetching library solves these directly; reimplementing them by hand in useEffect + useState is the common anti-pattern this decision tree is steering away from.
Interview Drills
- Why would a large multi-field form prefer uncontrolled inputs over controlled ones? — Controlled inputs re-render the component on every keystroke of every field; for a form with many fields where nothing else in the UI needs to react to interim values as the user types, this is unnecessary re-render cost — uncontrolled inputs let the DOM handle typing natively and only read values when actually needed (e.g., on submit).
- Why is server-fetched data treated as a different category of state than client-only UI state in the decision tree? — Server data has genuinely different concerns — it can go stale, needs refetching, benefits from deduplication of identical in-flight requests, and often needs background revalidation — none of which apply to purely local UI state like "is this modal open." A dedicated data-fetching library (React Query) solves these directly rather than requiring them to be hand-rolled with
useEffect. - A prop is passed through 4 layers of components that don't use it themselves, just to reach a deeply nested child. What's the standard first fix to consider, and when would you escalate beyond it? — First reach for lifting state up / Context if the value is simple and infrequently updated (theme, current user); escalate to a dedicated state library specifically when updates are frequent enough that Context's "every consumer re-renders on any Provider value change" behavior becomes a real performance problem, not just a stylistic one.
Cross-links
[[React Hooks Deep-Dive]] · [[OOD Design Patterns]] · [[SOLID Principles]] · [[React Performance & Reconciliation]]