Around three years, React interviews shift from "can you build it" to "did you make the right calls." The questions below expect design judgement — when to extract a hook, when memoization earns its cost, how Context behaves under load, and how you keep state architecture from rotting. Answer with the trade-off, not just the mechanism, and ground each point in something you have shipped.
What the 3-year React interview really tests
Interviewers here are calibrating your judgement. Two candidates can both know useMemo; the stronger one explains when not to use it. When you get a scenario, state the options, name the cost of each, then commit to one and say why. That "options → cost → decision" shape is the signal that separates this band from the two-year answers.
Q1. When and how do you extract a custom hook?
Extract a custom hook when the same stateful logic — subscriptions, form handling, data fetching, timers — appears in more than one component, or when a component's logic is complex enough that naming it improves readability. A custom hook is just a function starting with use that calls other hooks; it shares logic, never state instances, so each caller gets its own independent state.
The point candidates miss is that custom hooks do not share state, only behaviour. Two components calling useToggle() each get a separate toggle.
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn(o => !o), []);
return [on, toggle];
}
// Each caller has its own independent `on`
Interview note: Follow-up: "what stays in the component?" JSX and anything tightly coupled to this component's markup. The hook owns reusable logic; the component owns presentation.
Q2. useMemo vs useCallback — what is the real difference, and when do they help?
useMemo caches a computed value; useCallback caches a function reference. useCallback(fn, deps) is exactly useMemo(() => fn, deps). They help in three cases: an expensive computation you do not want to repeat, a stable reference for a React.memo child so it does not re-render, and a stable dependency for another hook. On cheap values they cost more than they save.
The mature answer is that memoization is not free — it holds references in memory and adds a dependency array to maintain. The default should be no memoization; you add it when a profile shows a real problem.
const sorted = useMemo(
() => [...items].sort((a, b) => a.rank - b.rank),
[items] // recompute only when items change
);
const onSelect = useCallback((id) => dispatch(select(id)), [dispatch]);
Interview note: Trap: "will
useCallbackstop a child re-rendering?" Only if the child is wrapped inReact.memoand all its other props are also stable. A stable callback alone does nothing if the parent still passes fresh props.
Q3. Why does Context cause re-renders, and how do you contain it?
Every component consuming a Context re-renders whenever the Context value changes — and if you pass a fresh object literal as the value, that is every render. Contain it by memoizing the value, splitting one big Context into several focused ones, or separating rarely-changing data from frequently-changing data so consumers only subscribe to what they need.
This is a favourite three-year question because it exposes whether you understand Context's cost. The classic mistake is a single AppContext holding user, theme and a live counter — every consumer re-renders on every tick.
const value = useMemo(() => ({ user, logout }), [user, logout]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
Interview note: Follow-up: "does
useContextsupport selecting part of the value?" No — that is why you split contexts or reach for a store with selectors; see the Context API questions.
Q4. useState vs useReducer — how do you choose?
Use useState for independent, simple values. Reach for useReducer when state transitions are complex, when the next state depends on the previous in non-trivial ways, when multiple values change together, or when you want to test state logic in isolation from the component. The reducer centralises transitions into one pure function, which is easier to reason about and test than several scattered setters.
function reducer(state, action) {
switch (action.type) {
case "submit": return { ...state, status: "loading", error: null };
case "success": return { ...state, status: "done", data: action.data };
case "error": return { ...state, status: "error", error: action.error };
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, { status: "idle" });
Interview note: Trap: "reducers must be pure — why does that matter?" Because React may call them in development twice (Strict Mode) to surface side effects; a reducer that mutates or fetches breaks unpredictably.
Q5. How do you decide where state lives — local, Context or a store?
Keep state as local as possible; lift it only as far as the components that need it; use Context for low-frequency global data like auth, theme and locale; and reach for a store like Redux or Zustand when you have high-frequency shared state, complex updates, or need devtools and middleware. The cost ladder is the answer: local is free, Context re-renders all consumers, a store adds boilerplate but adds selectors and tooling.
The state management questions go deeper, but at three years the expectation is a defensible decision, not a memorised hierarchy.
Interview note: Follow-up: "server data — where does it go?" Ideally not in client state at all; a data library caches it separately from UI state, which removes most 'global state' entirely.
Q6. How do you test a React component, and what do you avoid testing?
Test behaviour, not implementation: with React Testing Library, query by accessible role or text, simulate real user events, and assert on what the user sees or what a callback received. Avoid asserting on internal state variables, hook call counts or class names — those break on refactors that do not change behaviour.
test("submits the email", async () => {
render(<Signup onSubmit={onSubmit} />);
await userEvent.type(screen.getByRole("textbox", { name: /email/i }), "a@b.com");
await userEvent.click(screen.getByRole("button", { name: /sign up/i }));
expect(onSubmit).toHaveBeenCalledWith({ email: "a@b.com" });
});
Interview note: Follow-up: "why query by role?" It tests the way a user (and assistive tech) actually finds elements, so it doubles as a light accessibility check.
Q7. What is a stale closure at scale, and how do you design around it?
Beyond the beginner interval bug, stale closures bite in event subscriptions, debounced handlers and callbacks passed to third-party libraries that outlive a render. The senior-leaning fixes are to keep the latest value in a ref (useRef) for values you must read without re-subscribing, use the functional updater for state, and keep effect dependencies honest. Explaining the ref pattern for "latest callback" is a strong three-year signal.
Interview note: Trap: "why not just disable the exhaustive-deps lint rule?" Because the rule is usually right — silencing it hides the stale-closure bug instead of fixing the design.
Q8. How do you handle errors so one broken component does not blank the page?
Wrap risky subtrees in an error boundary — a component that catches render errors below it and shows a fallback. Error boundaries catch render, lifecycle and constructor errors, but not errors in event handlers or async code, which you handle with try/catch and state. Knowing that split — boundaries for render, try/catch for events — is exactly the distinction interviewers probe.
Interview note: Follow-up: "are error boundaries hooks yet?" There is no hook form; you use a class component with
getDerivedStateFromError/componentDidCatch, or a library wrapper around it.
Q9. How do you avoid unnecessary re-renders in a real feature?
Measure first with the Profiler, then apply the right fix: colocate state so updates do not cascade, split components so a fast-changing piece does not re-render a slow one, memoize the truly expensive parts, and stabilise props for memoized children. The mature framing is that most re-renders are cheap and correct; you optimise the measured hot path, not everything. The performance optimization questions drill this further.
Interview note: Trap: "is wrapping everything in
React.memoa good default?" No — it adds prop-comparison cost and memory everywhere and usually helps nowhere. Target it.
How to prepare
Take one non-trivial component you have shipped and practise refactoring it live: extract a custom hook, justify each memoization, decide where its state belongs, and name what you would test. That single exercise touches most of the questions above. Then rehearse the "options → cost → decision" shape until it is automatic, because at three years the reasoning is the answer. When you are ready to stretch, review the 5-year question set to see how these topics scale into architecture, and deepen the fundamentals through the React learning path. A mock interview focused on trade-off questions is the fastest way to find where your reasoning still hesitates.
Frequently Asked Questions
What separates a 3-year React answer from a 2-year one?
When does useMemo actually help?
Do I need to explain custom hooks at 3 years?
How much testing is expected at this level?
Is state management library knowledge required at 3 years?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — See the Java Full Stack course in Hyderabad

