Senior Extras
The topics interviewers add on top of the standard list. Each one is short — you only need enough to answer confidently and show you keep up.
useReducer, and when to prefer it over useState
Redux's pattern, locally: you dispatch actions to a reducer instead of calling five different setters.
function reducer(state, action) {
switch (action.type) {
case "loading": return { ...state, status: "loading", error: null };
case "success": return { ...state, status: "success", data: action.payload };
case "error": return { ...state, status: "error", error: action.payload };
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, { status: "idle", data: [], error: null });
dispatch({ type: "loading" });
Use it when: three or more fields change together, the next state depends on
the previous one, or the transitions are rules you'd like to see in one place — a form wizard,
a data-fetch state machine, a cart. Stick with useState for
independent simple values.
useReducer centralises state transitions in a pure reducer, so instead of
scattering four setState calls through a handler I dispatch one action. I reach
for it when several fields always change together or the transitions are genuinely rules —
a wizard, a request state machine. It's also easier to test, since the reducer is a pure
function.
What's new in React 19?
| Feature | What it does |
|---|---|
| The React Compiler | Memoizes automatically at build time — far less manual useMemo/useCallback |
Actions + useActionState | Form submission with pending state, errors and optimistic updates built in |
useOptimistic | Show the result instantly, roll back if the server rejects it |
use() | Read a promise or context inside render — and it can be called conditionally |
ref as a normal prop | forwardRef is no longer needed for function components |
| Document metadata | <title> and <meta> can be rendered anywhere — no react-helmet |
You don't need to have shipped React 19. Mentioning the compiler and ref as a prop in one sentence is enough to show you follow the ecosystem — and it pairs perfectly with the "don't over-memoize" answer in Module 05.
React Query vs Redux — server state vs client state
They're not competitors. React Query owns server state (cached copies of API data); Redux owns client state (things only your UI knows).
| Server state | Client state | |
|---|---|---|
| Examples | Brand list, order details, dashboard numbers | Cart before checkout, wizard step, sidebar open, theme |
| Who owns the truth | The database — your copy can go stale | The browser — it's the truth |
| Needs | Caching, refetch, retry, dedupe, invalidation | Predictable transitions |
| Tool | React Query / RTK Query | Redux / Zustand / Context |
// most Redux boilerplate for API data disappears
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ["brands", page],
queryFn: () => getBrands(page)
});
You get caching, background refetching, retries, request de-duplication and the whole loading/error state machine (Q80) — no store, actions or thunks for data that the server owns anyway.
Most of what teams put in Redux is actually cached server data, which has completely
different needs — staleness, refetching, retries, invalidation. React Query handles that, so
Redux is left for genuine client state like a wizard or a cart. Separating the two removes a
lot of boilerplate and a whole class of stale-data bugs.
Stale closures and fetch race conditions
Two classic async bugs: an effect remembering old values, and a slow old response overwriting a newer one.
Q85 · Stale closure
useEffect(() => {
const t = setInterval(() => setCount(count + 1), 1000); // count is frozen at 0 forever
return () => clearInterval(t);
}, []);
useEffect(() => {
const t = setInterval(() => setCount(c => c + 1), 1000); // updater form always sees the latest
return () => clearInterval(t);
}, []);
A function captures the values from the render that created it. With [] it captures
the first render's values — forever.
Q86 · Race condition
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(r => r.json())
.then(setUser)
.catch(e => { if (e.name !== "AbortError") setError(e); });
return () => controller.abort(); // cancel the old request when userId changes
}, [userId]);
Race conditions are invisible on a fast local network and appear only in production on a slow
connection. Mentioning AbortController in the cleanup signals you've debugged
real problems, not just built demos. (React Query handles this for you automatically.)
StrictMode, useLayoutEffect, and the Rules of Hooks
Q87 · Why effects run twice in development
React 18+ StrictMode deliberately mounts a component, unmounts it and mounts it again in development. It's not a bug — it's a test of whether your cleanup is correct. If double-mounting breaks something, you had a missing cleanup. It does not happen in production builds.
Q88 · useLayoutEffect vs useEffect
| useEffect | useLayoutEffect | |
|---|---|---|
| Runs | After the browser paints | Before the paint |
| Blocks painting? | No | Yes |
| Use for | Everything (default) | Measuring the DOM, positioning a tooltip, restoring scroll — anything that would visibly flicker |
Rule: default to useEffect; switch only when you see a flicker.
Q89 · The Rules of Hooks
- Call hooks only at the top level — never inside an
if, a loop or a nested function. - Call them only from React function components or other hooks.
Why: React tracks hooks by call order, not by name. If a conditional skips one, every hook after it shifts and gets another hook's state. Need a conditional? Put the condition inside the hook:
if (userId) { useEffect(...) } // breaks the order
useEffect(() => { if (userId) {...} }, [userId]); // correct
Three patterns worth knowing by name
Q90 · Reset a component with key
<EditUserForm key={selectedUserId} user={selectedUser} />
When the key changes, React unmounts the old component and mounts a fresh one — all internal state is reset. It's the clean alternative to syncing props into state inside an effect.
Q91 · Composition instead of prop drilling
// instead of threading user through Layout → Header → UserMenu
<Layout header={<UserMenu user={user} />} />
// or
<Layout><UserMenu user={user} /></Layout>
The middle layers never see user at all. Reach for this before Context.
Q95 · Why state must be updated immutably
items.push(newItem); setItems(items); // same reference → React sees no change → no re-render
setItems([...items, newItem]); // new reference → re-render
setUser({ ...user, name: "Ravi" }); // same for objects
React compares with Object.is — a reference check, not a deep comparison. Mutation
also breaks React.memo, useMemo and dependency arrays, all of which
rely on references changing.
How do you test React components?
React Testing Library with Vitest or Jest — test what the user sees and does, not the component's internals.
test("shows an error when the name is empty", async () => {
render(<BrandForm />);
await userEvent.click(screen.getByRole("button", { name: /save/i }));
expect(screen.getByText(/name is required/i)).toBeInTheDocument();
});
The five points to make:
- Query like a user:
getByRoleandgetByLabelTextfirst — that also enforces accessibility. Avoid test ids where you can. - Test behaviour, not state. Assert on what's rendered, so a refactor doesn't break every test.
- Mock the service layer (which is easy precisely because it exists, Q43), or use MSW to intercept at the network level.
userEventoverfireEvent— it simulates real typing, focus and clicks.- Cover the branches that matter: loading, error, empty and content — the four states from Q80.
How do you structure a large React project?
By feature, not by file type — and with one API client that every service goes through.
The principles to state out loud:
- Pages stay dumb, services stay smart. A page never builds a URL or touches a token.
- One place for cross-cutting concerns — auth, base URL, error shape.
- Feature folders scale. With twenty modules, "everything about stickers is in one folder" beats "all 200 components in one components/ folder".
- Types mirror the backend DTOs, so a renamed field is a compile error, not a production bug.
You've actually built this — an admin portal with 20+ feature modules, a service layer, a
single fetch-based API client with a consistent { isSuccess, data, messageCode }
envelope, Context for auth/theme/sidebar, and a ProtectedRoute guard. Describe
it from experience rather than theory, and add what you'd improve: React Query for caching,
lazy-loaded routes, and a shared usePagedList hook.
Four quick ones
Q94 · Avoiding unnecessary re-renders from Context
Every consumer re-renders when the context value changes, and
value={{ user, setUser }} is a new object every render. Fix it by memoizing the
value and splitting contexts by concern, so a theme change doesn't re-render the whole app:
const value = useMemo(() => ({ user, login, logout }), [user]);
Q96 · CSR vs SSR, and where Next.js fits
| CSR (Vite SPA) | SSR / SSG (Next.js) | |
|---|---|---|
| First response | An empty HTML shell, then JS builds the page | Ready-made HTML from the server |
| SEO | Weak | Strong |
| Good for | Admin panels, dashboards behind a login | Public sites, marketing pages, e-commerce |
An internal admin portal behind a login has no SEO requirement, so CSR is the right call — being able to justify that is the point.
Q97 · Synthetic events
React wraps native DOM events in a cross-browser SyntheticEvent and attaches
one listener at the root instead of one per element. You get consistent behaviour everywhere,
camelCase handlers like onClick, and e.preventDefault() /
e.stopPropagation() work as normal.
Q98 · Debouncing
Waiting until the user stops typing before acting — so a search box makes one API call instead
of one per keystroke. Implemented as a useDebounce hook with
setTimeout and a cleanup that cancels the previous timer (the live example is in
Module 11). Also used for autosave and resize handlers.
Throttle is the sibling: run at most once every N ms, for scroll handlers.
When you don't know something, say so and then say how you'd find out — that reads as senior. When you do know it, answer in three beats: what it is → why it matters → what you'd do in practice. And bring every answer back to something you've actually built. Good luck, Ajay.