Interview Prep · 2026
React interview questions and answers
Preparing for a React JS interview? These React interview questions cover hooks, memoization, custom hooks, context and FAANG-level topics. Each one has a short answer, a plain-English analogy and working code. Want to try an idea first? Open our online React compiler, then practice with react coding challenges that are checked by real tests.
More guides:Experienced developer questionsMachine coding round guideMock interview guideCoding questions with solutions
12 questions6 topics0 bookmarked
Hooks & State
3 questionsShort answer
useState is for simple, mostly independent values. useReducer is for related pieces of state that change together through a small set of named actions — it makes the transitions explicit and testable.Analogy
useState is like keeping loose sticky notes on your desk — great for one or two things. useReducer is a control panel with labelled buttons: every state change goes through a known lever, so you always know how you got here.example.tsxReact
// useState: fine for one value
const [count, setCount] = useState(0);
// useReducer: for related transitions
type Action = { type: "add"; by: number } | { type: "reset" };
function reducer(state: number, action: Action) {
switch (action.type) {
case "add": return state + action.by;
case "reset": return 0;
}
}
const [count, dispatch] = useReducer(reducer, 0);
dispatch({ type: "add", by: 1 });Hooks & State
Short answer
State values in React are snapshots that belong to the render they were read in. Calling setState schedules an update — it does not mutate the current variable. The new value shows up on the next render.Analogy
Reading `count` inside a handler is like reading a photograph. Even after you ask the camera to take a new one, you're still holding the old print in your hand.example.tsxReact
function Counter() {
const [count, setCount] = useState(0);
function onClick() {
setCount(count + 1);
console.log(count); // still the OLD value
// To read the freshest value, use the updater form:
setCount(c => c + 1);
}
return <button onClick={onClick}>{count}</button>;
}Hooks & State
Short answer
useEffect runs after the browser has painted — good for most side effects. useLayoutEffect runs synchronously after DOM mutations but before paint — use it when you need to measure or mutate the DOM without a visible flicker.Analogy
useEffect is a stagehand who tidies up after the curtain closes. useLayoutEffect is one who runs on stage between scenes to move props before the audience ever sees the new layout.example.tsxReact
function Panel() {
const ref = useRef<HTMLDivElement>(null);
const [h, setH] = useState(0);
// Measures + writes before paint — no flicker
useLayoutEffect(() => {
if (ref.current) setH(ref.current.getBoundingClientRect().height);
}, []);
return <div ref={ref} style={{ paddingTop: h }}>…</div>;
}Hooks & State
Custom Hooks
2 questionsShort answer
The prefix is a convention enforced by the ESLint Rules of Hooks plugin so it can check that hooks are only called at the top level of a component or another hook. React itself doesn't require the name.Analogy
It's a uniform. The 'use' prefix is how the linter recognises hook staff at the door and enforces the safety rules that apply to them.example.tsxReact
// The linter treats this as a hook because of the 'use' prefix
function useOnlineStatus() {
const [online, setOnline] = useState(navigator.onLine);
useEffect(() => {
const up = () => setOnline(true);
const down = () => setOnline(false);
window.addEventListener("online", up);
window.addEventListener("offline", down);
return () => {
window.removeEventListener("online", up);
window.removeEventListener("offline", down);
};
}, []);
return online;
}Custom Hooks
Short answer
Keep the latest callback in a ref. Update the ref every render, and have the interval read from the ref — that way it always calls the current logic without needing to be torn down and rebuilt.Analogy
The interval is a phone that dials the same number every second. Instead of buying a new phone when the callback changes, you just forward the number to a new voicemail.example.tsxReact
export function useInterval(cb: () => void, delay: number | null) {
const saved = useRef(cb);
useEffect(() => { saved.current = cb; }, [cb]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => saved.current(), delay);
return () => clearInterval(id);
}, [delay]);
}Custom HooksPractice this →
Performance
3 questionsShort answer
Use useMemo when the computation is genuinely expensive OR when the result is a reference (object/array) that a downstream memoized child or dependency array needs to stay stable. Otherwise it's overhead, not optimization.Analogy
useMemo is a cache. Caches are useful when the cost of missing the cache is higher than the cost of maintaining it. For a trivial calculation, the cache itself is the expensive part.example.tsxReact
// Worth memoizing: heavy compute
const stats = useMemo(() => expensiveAggregate(rows), [rows]);
// Worth memoizing: stable reference for a memoized child
const options = useMemo(() => ({ locale: "en", tz }), [tz]);
return <Chart options={options} />;
// NOT worth memoizing
const doubled = useMemo(() => n * 2, [n]); // just: const doubled = n * 2;Performance
Short answer
useCallback returns a memoized function reference. useMemo returns a memoized value. They're the same idea — in fact useCallback(fn, deps) is just sugar for useMemo(() => fn, deps).Analogy
useMemo hands you the finished cake. useCallback hands you the same recipe card each time so nobody downstream thinks the recipe changed.example.tsxReact
// These are equivalent: const onSave = useCallback(() => save(id), [id]); const onSave2 = useMemo(() => () => save(id), [id]); // useMemo for a value: const total = useMemo(() => items.reduce((a, i) => a + i.price, 0), [items]);
Performance
Short answer
React.memo does a shallow prop comparison. If the parent recreates any object, array, or function prop on every render, the reference changes and memo bails. Wrap those props in useMemo / useCallback so the reference is stable.Analogy
React.memo checks if the box is the same box, not whether it has the same contents. A fresh box every render always looks different, even if what's inside is identical.example.tsxReact
// Broken: new arrow function each render → List re-renders every time
<List items={items} onPick={(x) => setPick(x)} />
// Fixed: stable reference
const onPick = useCallback((x) => setPick(x), []);
<List items={items} onPick={onPick} />PerformancePractice this →
Context & Patterns
1 questionShort answer
Split when frequently-changing state lives in the same provider as rarely-changing state. Every consumer of the shared context re-renders when any value in it changes — separating them scopes re-renders to only the consumers that care.Analogy
A shared group chat means everyone hears every ping. Splitting into topic channels means only the people who care about that topic get pinged.example.tsxReact
// Before: one big context — theme changes re-render every cart consumer
const AppCtx = createContext({ user, theme, cart });
// After: split by change frequency
const UserCtx = createContext(user); // rarely changes
const ThemeCtx = createContext(theme); // rarely changes
const CartCtx = createContext(cart); // changes often
// Cart consumers only re-render when cart changes.Context & PatternsPractice this →
Forms & Events
1 questionShort answer
In a controlled input, React state is the source of truth: the value comes from state and updates via onChange. In an uncontrolled input, the DOM holds the value and you read it with a ref when you need it.Analogy
Controlled is a live-transcribed interview — every word goes through the notepad. Uncontrolled is a voice recorder you only listen to at the end.example.tsxReact
// Controlled
const [name, setName] = useState("");
<input value={name} onChange={(e) => setName(e.target.value)} />
// Uncontrolled
const ref = useRef<HTMLInputElement>(null);
<input ref={ref} defaultValue="Ada" />
<button onClick={() => console.log(ref.current?.value)}>Read</button>Forms & EventsPractice this →
FAANG Interview Set
2 questionsShort answer
In development, Strict Mode intentionally mounts, unmounts, and remounts each component to surface effects that aren't properly cleaned up. It never happens in production builds. The fix is to write a correct cleanup function.Analogy
It's a fire drill: the building runs the alarm on purpose so the wiring gets tested before there's a real fire.example.tsxReact
useEffect(() => {
const socket = connect(url);
socket.on("message", handle);
return () => {
socket.off("message", handle); // cleanup is what makes double-invoke safe
socket.close();
};
}, [url]);FAANG Interview SetAsked at Meta
Short answer
In React 18+ with automatic batching, both cases batch the same way — every setState inside the same tick is coalesced into one render. Before React 18, updates inside setTimeout / promises did NOT batch; that older behaviour is the source of this classic trick question.Analogy
React 18 gave every entrance the same bouncer. Before 18, the side door (setTimeout) let people in one at a time even when the main door was batching them.example.tsxReact
// React 18+: one render in BOTH cases
function onClick() {
setA(1);
setB(2); // batched
}
setTimeout(() => {
setA(1);
setB(2); // ALSO batched in React 18+
}, 0);FAANG Interview SetAsked at Vercel
Reading answers is only half the prep
Practice these topics with real coding challenges checked by hidden tests.
Upgrade To Pro →