12 questions on React: components and props, state and batching, effects, keys, context, refs, memoization, error boundaries, code splitting, and stale closures.
01ReactFoundation
Components and props
Answer
A component is a function (or class) that returns React elements describing UI from props and state.
Props are read-only inputs from the parent. Treat them as immutable. Do not mutate props objects or arrays.
Compose small components instead of one large view. Pass data down and events up through callbacks.
Children are a normal prop. Use them to wrap layout or shared chrome around nested content.
Name components with uppercase so React treats them as components, not DOM tags.
Pure rendering: given the same props and state, the returned UI description should match.
Pitfall: mutating a prop and expecting the parent to see the change. Lift state or pass a setter callback instead.
Default props and destructuring keep call sites clear. Prefer explicit props over hidden globals.
Interview tip: explain one-way data flow from parent props into child UI.
useEffect connects a component to an external system (such as browser DOM APIs, network sockets, window event listeners, or non-React libraries). It runs after the browser paints the screen to avoid blocking visual updates.
Not for Derived State: Do not use useEffect to calculate state that can be derived directly during rendering. Calculating derived state inside an effect causes an unnecessary second render cycle and risks state desynchronization.
Cleanup Function: Returning a function from useEffect schedules cleanup to run right before the effect re-runs with new dependencies, and when the component unmounts. Essential for aborting fetches, clearing timers, and removing event listeners.
React 18 Strict Mode Development Behavior: In development mode, React deliberately mounts, unmounts, and re-mounts every component to verify that effect cleanups are correctly implemented and do not cause resource leaks.
Flow diagram: useEffect execution and cleanup cycle across renders
TypeScript (React 18+)
import { useState, useEffect } from 'react';
interfaceUserProfileProps {
userId: string;
}
exportfunctionUserProfile({ userId }: UserProfileProps) {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = newAbortController();
asyncfunction loadUser() {
try {
const res = await fetch(`/api/users/${userId}`, { signal: controller.signal });
if (!res.ok) thrownewError(`HTTP ${res.status}`);
const data = await res.json();
setUser(data);
} catch (err: any) {
if (err.name !== 'AbortError') setError(err);
}
}
loadUser();
// Cleanup runs before next effect execution or upon component unmountreturn () => controller.abort();
}, [userId]);
if (error) return <div>Error loading user: {error.message}</div>;
if (!user) return <div>Loading...</div>;
return <h1>{user.name}</h1>;
}
Dependencies and Stale Closures
Exhaustive Dependencies Rule: Every reactive value (props, state, or functions declared in the component body) read inside an effect must be included in the dependency array.
Stale Closure Risk: Omitting dependencies causes the effect closure to capture outdated prop and state values from older renders, leading to bugs like timers ticking with frozen numbers.
08ReactIntermediate
Context
Answer
Context passes data through the tree without threading props through every intermediate component.
Create a context, wrap a subtree in Provider, and read with useContext (or Context.Consumer in older code).
All consumers re-render when the provider value changes. Value identity matters.
Split contexts by concern (theme, auth, locale) so unrelated updates do not refresh the whole tree.
Memoize the provider value object when you pass an object or callbacks and updates are frequent.
Prefer props for local parent-child data. Use context for cross-cutting values many levels deep.
Pitfall: putting the entire app state in one context and updating often, which re-renders large subtrees.
Default context values apply only when no Provider sits above the consumer. Do not rely on defaults for required auth data.
Interview tip: contrast context with prop drilling and with a state library when the update graph is complex.
Flow diagram: Context
TSX
constAuthContext = createContext<Auth | null>(null);
exportfunction useAuth() {
const value = useContext(AuthContext);
if (!value) thrownewError("useAuth needs AuthProvider");
return value;
}
09ReactIntermediate
useMemo and useCallback
Answer
useMemo caches a computed value between renders until its dependency list changes.
useCallback caches a function identity for child memoization, effect dependencies, or stable event subscriptions.
React.memo wraps a component so it skips re-render when props are shallowly equal.
Measure first. Wrapping everything adds memory, dependency bookkeeping, and complexity.
Useful when passing callbacks to memoized children or building expensive derived data from large lists.
Pitfall: new object or array props ({...}) break memoization even when contents look the same.
Dependencies must be correct. Wrong deps return stale memoized values, the same class of bug as stale effects.
In many apps, lifting state and splitting components removes the need for heavy memoization.
Follow the official guidance: memoize for proven costs, not by default on every function.
React State, Fiber Architecture, and Automatic Batching
Answer
State in React represents values that change over time and trigger a component re-render when updated.
Immutability Requirement: React relies on shallow object reference equality (Object.is) during reconciliation to determine whether state has changed. Mutating state objects or arrays in-place (such as state.items.push(newItem)) does not change the memory reference of the array, causing React to miss the change and skip the re-render. Always treat state as read-only and return new copies using the spread operator or immutable update patterns.
React 18 Automatic Batching: In React 18, all state updates are automatically batched together into a single re-render, whether they originate inside React event handlers, asynchronous fetch promises, setTimeout callbacks, or native event listeners.
Fiber Tree Reconciliation: React maintains an in-memory virtual tree of Fiber nodes representing component instances, DOM nodes, and state queues. When state updates, React constructs a work-in-progress Fiber tree, computes minimal DOM diffs, and commits the updates to the real browser DOM in a single flush.
Flow diagram: State mutation failure vs immutable copy reconciliation
A closure in JavaScript captures variables from its lexical environment at the time of creation. In React, a function component re-executes on every render, creating a fresh scope with current props and state.
How Closures Become Stale: When an asynchronous callback (such as setTimeout, setInterval, or a Promise continuation) captures a state variable, it holds a reference to the variable instance from that specific render. If the component re-renders and state updates, the older callback continues referencing the outdated value from the render in which it was created.
Flow diagram: Closure capturing render scope vs mutable ref
TypeScript (Fixing Stale Closures)
import { useState, useEffect, useRef } from 'react';
exportfunctionTimer() {
const [count, setCount] = useState(0);
// Solution 1: Functional update (Preferred for state updates)
useEffect(() => {
const id = setInterval(() => {
setCount(prevCount => prevCount + 1); // Always reads latest queued state
}, 1000);
return () => clearInterval(id);
}, []); // Empty deps safe because no closure over count is required// Solution 2: useRef for callbacks needing latest values without re-running effectconst countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const id = setInterval(() => {
console.log("Latest count:", countRef.current);
}, 1000);
return () => clearInterval(id);
}, []);
return <div>Count: {count}</div>;
}
Solutions: Functional State Updates and useRef
Functional State Updates: When updating state based on previous state, pass an updater lambda: setCount(prev => prev + 1). This bypasses the closure value and receives the current state value directly from React's internal state queue.
useRef Escape Hatch: Unlike state, ref objects hold a mutable .current property that maintains the exact same object reference across all renders. Reading ref.current inside an asynchronous callback always accesses the latest value.
12ReactAdvanced
Advanced: index as key
Answer
key={index} is safe only when the list is static: no reorder, no insert in the middle, no delete of earlier items.
When the list changes shape, React can reuse the wrong component instance because the key stayed with the index, not the item.
Local state inside a row (controlled inputs, open flags) then appears on another row after insert or delete.
Use a stable id from your data. Create ids when the item is born, not during each render.
After filtering, still key by the item id, not by the filtered index.
If your API has no id, generate one when the item enters client state and keep it on the object.
Pitfall: using index keys in sortable to-do lists or tables, which are exactly the cases that break.
Indexes as keys can look fine in demos until the first reorder. Test with insert at index 0.
This is the same reconciliation rule as react-lists-keys, focused on the common interview failure mode.
Flow diagram: Index key remaps identity
TSX
// Bad when list can reorder
{items.map((item, i) => <Row key={i} item={item} />)}
// Good
{items.map((item) => <Row key={item.id} item={item} />)}