Module 13 · Senior Extras
Module 13 · Questions 81–98 · Beyond the list

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.

Q81 · Q82

useReducer, and when to prefer it over useState

In one line

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.

Say it like this

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.

Q83

What's new in React 19?

FeatureWhat it does
The React CompilerMemoizes automatically at build time — far less manual useMemo/useCallback
Actions + useActionStateForm submission with pending state, errors and optimistic updates built in
useOptimisticShow 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 propforwardRef is no longer needed for function components
Document metadata<title> and <meta> can be rendered anywhere — no react-helmet
How to use this in the interview

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.

Q84

React Query vs Redux — server state vs client state

In one line

They're not competitors. React Query owns server state (cached copies of API data); Redux owns client state (things only your UI knows).

Server stateClient state
ExamplesBrand list, order details, dashboard numbersCart before checkout, wizard step, sidebar open, theme
Who owns the truthThe database — your copy can go staleThe browser — it's the truth
NeedsCaching, refetch, retry, dedupe, invalidationPredictable transitions
ToolReact Query / RTK QueryRedux / 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.

Say it like this

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.

Q85 · Q86

Stale closures and fetch race conditions

In one line

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

user clicks user 1 → request A starts (slow) user clicks user 2 → request B starts (fast) B returns → screen shows user 2 ✅ A returns → screen shows user 1 ❌ WRONG — the old one won
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]);
Why this answer lands well

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.)

Q87 · Q88 · Q89

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

useEffectuseLayoutEffect
RunsAfter the browser paintsBefore the paint
Blocks painting?NoYes
Use forEverything (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
Q90 · Q91 · Q95

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.

Q92

How do you test React components?

In one line

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:

  1. Query like a user: getByRole and getByLabelText first — that also enforces accessibility. Avoid test ids where you can.
  2. Test behaviour, not state. Assert on what's rendered, so a refactor doesn't break every test.
  3. Mock the service layer (which is easy precisely because it exists, Q43), or use MSW to intercept at the network level.
  4. userEvent over fireEvent — it simulates real typing, focus and clicks.
  5. Cover the branches that matter: loading, error, empty and content — the four states from Q80.
Q93

How do you structure a large React project?

In one line

By feature, not by file type — and with one API client that every service goes through.

src/ ├── api/ api-client.ts ← base URL, JWT header, error envelope ├── services/ brandService.ts, orderService.ts ← endpoints per feature ├── types/ brand.ts, order.ts ← mirrors the backend DTOs ├── pages/ │ ├── brands/ BrandsPage, BrandFormPage, BrandDetailPage │ └── orders/ OrdersPage, OrderDetailPage ├── components/ ui/ · form/ · tables/ · charts/ ← shared, dumb ├── context/ AuthContext, ThemeContext ├── hooks/ useDebounce, useModal, usePagedList ├── routes/ ProtectedRoute └── layout/ AppLayout, AppSidebar, AppHeader

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.
This is your strongest answer

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.

Q94 · Q96 · Q97 · Q98

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 responseAn empty HTML shell, then JS builds the pageReady-made HTML from the server
SEOWeakStrong
Good forAdmin panels, dashboards behind a loginPublic 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.

Last thing before the interview

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.