Module 10: Interview Questions
Hooks, Rendering and Architecture: Questions and Answers
The intermediate-level questions about effects, closures, memoization, context and data fetching, answered with the reasoning an interviewer is listening for.
These are the questions that separate someone who has built a toy app from someone who has shipped and debugged a real one. Most of them circle around two ideas: closures (what a function remembers from its render) and identity (what counts as "the same" value between renders). If you keep those two in mind, most answers fall out naturally.
What are the rules of hooks, and why do they exist?#
Only call hooks at the top level of a component or custom hook, never inside conditions, loops or nested functions, and only call them from React functions.
The reason is how React stores hook state. It doesn't know variable names. It keeps an ordered list of slots for each component, and each hook call takes the next slot. If a hook is skipped on some render, every later hook reads the wrong slot. The linter plugin enforces the rules so you don't have to rely on memory.
How does the dependency array of useEffect work?#
After each render, React compares each dependency with its value from the previous render using Object.is. If any is different, it runs the cleanup from the previous effect, then runs the effect again. No array means run after every render. An empty array means only after the first. The array should list every reactive value the effect reads, which the exhaustive-deps lint rule checks.
A point worth making: the array isn't a performance dial. It's a statement of what the effect depends on. If you leave something out to make the effect run less often, you've got a stale value bug waiting.
What is the cleanup function for?#
It undoes whatever the effect set up: clearing timers, removing listeners, closing connections, aborting requests. React runs it before re-running the effect for new dependencies and when the component unmounts. Every setup should have a matching teardown, so that re-running the effect doesn't stack subscriptions.
Why does my effect run twice in development?#
Because of StrictMode. In development, React mounts the component, unmounts it and mounts it again, so effects run setup, cleanup, setup. It's a test that your effect is resilient. If running it twice causes visible problems like duplicate subscriptions or duplicate analytics events, the effect is missing its cleanup or is doing something that should happen in an event handler. It doesn't happen in production.
What is a stale closure, and how do you fix it?#
Each render creates new functions that capture that render's values. A callback created in an earlier render keeps seeing the old values. For example, a setInterval created in an effect with an empty dependency array that reads count always sees the first count.
Fixes, roughly in order of preference: use the updater form of the setter so you don't need to read the value (setCount((c) => c + 1)), include the value in the dependency array so the effect re-runs, store the latest value in a ref, or use useEffectEvent for logic that should always see the latest values without re-triggering the effect.
useRef or useState?#
Both persist across renders. Changing state triggers a re-render and its value is a snapshot per render. Changing ref.current does not trigger a re-render, and you can mutate it freely. So state is for anything displayed on screen, and refs are for values React doesn't need to know about: timer ids, previous values, DOM nodes, and mutable instances of external libraries. You shouldn't read or write a ref during rendering.
Explain React.memo, useMemo and useCallback.#
React.memo wraps a component so it's skipped when its props are shallowly equal to last time. useMemo caches a computed value between renders until its dependencies change. useCallback caches a function's identity until its dependencies change, and is equivalent to useMemo(() => fn, deps).
They work together. memo only helps if the props are actually stable, and useCallback and useMemo are how you keep object and function props stable. Using one without the other usually accomplishes nothing.
When should you not use useMemo and useCallback?#
For cheap calculations, and for functions passed to ordinary DOM elements or to components that aren't memoized, because you pay for the bookkeeping and gain nothing. The better first moves are structural: move state down to where it's used, or pass slow parts as children, so they don't re-render in the first place. And measure with the Profiler before and after.
Today I'd also mention the React Compiler, which applies this memoization automatically at build time for code that follows the rules of React, and so reduces how much of it you write by hand.
useState vs useReducer?#
They're the same mechanism. A reducer is better when there are many related values that change together, many kinds of transitions, or when the next state depends on complicated rules about the previous one. You gather the update logic into one pure function that's easy to test and reason about. For one or two independent values, useState is simpler.
How does context cause re-renders, and how do you limit that?#
Every component that calls useContext for a given context re-renders when the provider's value changes, compared by reference. Passing an inline object (value={{ a, b }}) creates a new reference on every render of the provider's parent, so all consumers re-render even if the contents didn't change.
To limit it: keep contexts small and focused, memoize the value with useMemo, split state and dispatch into separate contexts, put the provider in its own component that receives children, and for frequently updated, widely read state use a store with selectors, such as Zustand or Redux.
Do custom hooks share state between components?#
No. Each call to a custom hook creates its own independent state, exactly as if you'd pasted the hook's code into the component. A custom hook shares logic, not data. To share data you lift state up, use context, or an external store.
useEffect vs useLayoutEffect?#
useEffect runs after the browser has painted, so it doesn't block the screen update. useLayoutEffect runs synchronously after React has updated the DOM but before the browser paints. Use it when you must measure the DOM and change something before the user sees a frame, such as positioning a tooltip, to avoid a flicker. In most cases useEffect is right.
How do you handle race conditions when fetching in an effect?#
Responses can come back out of order. If a prop changes and you start a second request, the first might finish last and overwrite the correct data. In the effect, set a flag in the cleanup (ignore = true) and check it before setting state, or abort the request with an AbortController.
In practice I'd use a library like TanStack Query, which keys each request by its inputs so a stale response can never land in the wrong place, and gives caching, retries and deduplication as well.
How do you decide between Context, Redux Toolkit, Zustand and TanStack Query?#
I start by separating kinds of state. Server data (anything fetched and cached) goes to TanStack Query or RTK Query, not a client store. State that should survive refresh or be shared as a link goes in the URL. Local UI state stays in useState. For genuinely global client state: context for small, rarely changing values like theme or the current user; Zustand when I want a light store with selector-based subscriptions and no provider; Redux Toolkit on large teams or apps that benefit from strict structure, middleware and devtools.
What is code splitting, and how do you do it in React?#
Breaking the bundle into chunks that are loaded on demand, so users download less code up front. In React you use dynamic import() with React.lazy, wrapped in a Suspense boundary that shows a fallback while the chunk loads. The best place to start is route-level splitting, then heavy components that aren't always shown. Pair it with an error boundary in case a chunk fails to load.
How would you speed up a long list that renders slowly?#
First I'd profile to confirm where the time goes. Then, in order: make sure keys are stable, memoize row components (with stable props), avoid creating new objects in props, move state out of the parent so typing doesn't re-render all rows, and for really long lists use virtualization (TanStack Virtual or react-window) so only the visible rows exist in the DOM. For filtering and search, useDeferredValue or useTransition can keep the input responsive while the list catches up.
useTransition vs useDeferredValue?#
Both mark work as lower priority, so urgent updates like typing stay responsive. useTransition gives you a startTransition function that wraps a state update you control. useDeferredValue takes a value and returns a delayed copy of it, useful when the value comes from props or somewhere you can't wrap the update. React renders the urgent update first and works on the deferred one in the background, discarding it if newer input arrives.
What's the difference between client-side and server-side rendering?#
With client-side rendering, the server sends a nearly empty HTML page and a JavaScript bundle, and the browser builds the UI. That's what a Vite app does. With server-side rendering, the server renders the HTML first, so users see content sooner and search engines can read it, and then React "hydrates" the page, attaching event handlers to the existing markup. Frameworks such as Next.js and React Router's framework mode handle SSR for you. Which one you want depends on whether you need SEO and fast first paint for public content, or you're building an app behind a login.
Practice#
- Pick five questions and write a tiny code example for each that proves your answer, such as a stale-closure interval and its fix.
- For the "context re-renders" question, build the demo: a provider with an inline value, three consumers logging renders, and an unrelated state change in the provider's parent. Then fix it.
- Explain the difference between
memo,useMemoanduseCallbackto someone with no React experience, using an everyday analogy. If you can do that, you understand it.