Concurrent Rendering • Reconciler Corners • Server Rendering • Architecture • Leadership • 2026

React Interview Questions for 10+ Years Experience (Senior)

React interviews for 10+ years of experience skip what a hook is and go after what experience teaches: why concurrent rendering makes pure renders and consistent stores matter, where the reconciler keeps or throws away state, how portals, refs and effects behave at the edges, what streaming and server functions change, how to choose rendering and team boundaries across a big product, and how you lead migrations, standards and hard calls. It is written for React engineers with around eight to fifteen years of front-end work, interviewing for senior, staff or lead roles. Each question shows what the interviewer is checking, a shape for your answer and a sample you can adapt with your own stories.

Search all questions by round, difficulty and level, or save the ones you want to practice.

Rendering Internals 3 questions

Hard Technical round Senior Practice question

1. A dropdown renders its menu through a portal into document.body. Clicking a menu item also fires the onClick of the card that wraps the dropdown, and the click-outside logic closes the menu too early. Why?

What the interviewer is really testing:
Whether you know React events from a portal travel up the React tree, while DOM checks like contains follow the real DOM, and how those two clash.
Answer frame:

React tree: events fired inside a portal bubble to React ancestors as if the portal were rendered in place, so the card's onClick runs.

DOM tree: a click-outside check with node.contains looks at the real DOM, where the menu isn't inside the dropdown, so the click counts as outside.

Fix: make the click-outside check include the portal's own node, and stop propagation in the menu if the card must not react.

Sample spoken answer:

“A portal moves where the DOM nodes live, not where the component sits in the React tree. React's own events follow the React tree, so a click on a menu item bubbles to the dropdown and then to the card, exactly as if the menu were rendered inline. That's why the card's onClick fires. The click-outside logic is a native mousedown listener on the document that asks whether the dropdown's DOM node contains the target. In the real DOM the menu hangs off body, not the dropdown, so the answer is no, the menu closes on mousedown, and the item's click never lands. The two systems disagree. My fix is to keep a ref to the portal's container and treat a click inside either node as inside, and to call stopPropagation in the menu if the card really shouldn't react. For nested portals, like a submenu, each layer registers its node with the one above through context.”

Red flag to avoid:

Assuming events follow the DOM tree, or patching it with a setTimeout before closing the menu.

They may ask next:
  • Would a listener added with addEventListener on the card's DOM node see that click? Why or why not?
  • How would you make click-outside work for a submenu that opens its own portal?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

2. A video player restarts from the beginning whenever a promo banner appears. The only change was wrapping the player in an extra div while the banner shows. Why?

What the interviewer is really testing:
Whether you know the reconciler compares the tree position by position and throws away a whole subtree when the shape above it changes.
Answer frame:

Rule: when the element type at a position changes, React discards the old subtree and builds a new one; it doesn't search for moved children.

Here: the player moved from being a direct child to being inside a new div, so its position changed and it remounted.

Fix: keep the tree shape stable, for example always render the wrapper and change its class, or render the banner as a sibling.

Sample spoken answer:

“React's diff is deliberately simple. It compares the old and new trees position by position, and if the type at a spot changes, it throws away everything under it and mounts fresh. It doesn't look further down for the same component to move it. When the banner shows, the player goes from being a direct child to being a child of a new div, so from React's point of view the old player is gone and a new one appeared. The video element is recreated and starts from zero, and any state in the player is lost. The fix is to keep the tree shape the same in both cases: always render the wrapper and toggle a class or style, or put the banner next to the player instead of around it. For anything expensive to recreate, like a player or a map, I treat its position in the tree as part of its contract.”

Red flag to avoid:

Blaming the video element or the browser, without seeing that the tree shape changed.

They may ask next:
  • Would giving the player a stable key fix this? Why or why not?
  • Why doesn't React search the new tree for the old player and move it?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

3. You call a state setter with the exact same value, and the component function still runs one more time. Isn't React supposed to bail out when nothing changed?

What the interviewer is really testing:
Whether you know how the same-value bail-out really works, and why code must never depend on the exact number of renders.
Answer frame:

The comparison: React compares old and new state with Object.is, and if they match it skips the children and commits nothing.

The extra call: in some cases React has to call the component once more before it can confirm nothing changed, then discards that render.

The real trap: a new object or array with the same contents is a new reference, so it always re-renders.

Sample spoken answer:

“React compares the new value with the old one using Object.is, and if they're the same it skips re-rendering the children and commits nothing, so no effects run. But it can't always tell in advance. In some cases it has to call the component function one more time before it can confirm nothing changed, and then it throws that render away. So one extra call to the body, with nothing committed and no children rendered, is expected and harmless, as long as the render is pure. It only hurts if the render has side effects, like logging, which is one more reason render must be pure. The more common trap is the opposite one: setting a new object or array with the same contents. Object.is sees a new reference, so it always re-renders. In reducers and stores I return the existing state when nothing changed, and I never write logic that counts renders.”

Red flag to avoid:

Claiming React never calls the component when the value is the same, or writing logic that depends on render counts.

They may ask next:
  • Why does a reducer that returns a copy of unchanged state cause extra work?
  • How would you check how many times a component really renders versus commits?
Say it in 60 seconds

Concurrent React 3 questions

Hard Technical round Senior Practice question

4. Components read a home-grown store directly during render and subscribe in useEffect. During a slow transition, two of them show different values of the same data. Why, and what does useSyncExternalStore change?

What the interviewer is really testing:
Whether you understand tearing in concurrent rendering, how useSyncExternalStore prevents it, and the price it pays for that.
Answer frame:

Tearing: a concurrent render can pause between components, and the store can change in that gap, so one render reads two versions.

The hook: useSyncExternalStore checks that the snapshot didn't change during the render, and if it did, React renders again synchronously so the screen stays consistent.

The cost: store updates can't be treated as transitions, so a store change that makes content suspend shows the fallback instead of keeping the old screen.

Sample spoken answer:

“That's tearing. In a concurrent render React can pause between components to let the browser do other work. A store outside React can be changed during that pause, say by a socket message, so the components rendered before the pause read the old value and the ones after read the new one, and both end up on screen. The effect-based subscription can also miss a change that happens between render and the effect running. useSyncExternalStore closes both gaps. React reads the snapshot during render and checks it again before committing. If the store changed mid-render, it throws that work away and renders again synchronously, so everything matches. The price is that updates from that store are always urgent. They can't be time-sliced, and if one makes something suspend, React shows the fallback rather than keeping the old content the way a transition would. So I keep slow, suspending data out of these stores.”

Code:
const store = { state: { count: 0 }, listeners: new Set() };

function subscribe(callback) {
  store.listeners.add(callback);
  return () => store.listeners.delete(callback);
}

function useCount() {
  return useSyncExternalStore(subscribe, () => store.state.count);
}
Red flag to avoid:

Saying that wrapping the store in context fixes it, or not knowing that a concurrent render can pause between components.

They may ask next:
  • If the store's updates can't be transitions, how would you keep a slow screen that reads it responsive?
  • Why did state libraries have to change their internals when concurrent rendering arrived?
Say it in 60 seconds
Hard Technical round Senior Practice question

5. You wrapped a slow chart update in startTransition, but on low-end phones dragging the date slider still stutters. What can a transition interrupt, what can't it, and what do you do next?

What the interviewer is really testing:
Whether you know that concurrent rendering yields between components, not inside one, so a single heavy render still blocks the main thread.
Answer frame:

What it gives: a transition render can be interrupted by urgent updates, but React only checks between units of work, roughly between components.

Where it fails: one component doing heavy work in its body, like crunching thousands of points, still blocks until it finishes.

Next steps: profile, split the heavy work into smaller memoised pieces, draw less, or move the computation off the main thread.

Sample spoken answer:

“A transition doesn't make rendering cheaper. It marks an update as interruptible, so if the user drags again React can drop the half-finished render and start over. But React can only yield between units of work, which is roughly between components. If one chart component crunches ten thousand points in its body, that chunk runs to the end and the main thread is blocked the whole time. So on a slow phone the slider still waits. It also can't help if the slider's own value is updated inside the transition; that state has to stay urgent. My next step is the profiler, to see which component's render is long. Then I split the heavy part into smaller memoised pieces, cut the points down to what the screen can actually show, and if the maths itself is slow, move it to a worker or precompute it.”

Red flag to avoid:

Treating startTransition as a speed-up, or putting the input's own value inside the transition.

They may ask next:
  • When would you choose useDeferredValue over startTransition?
  • How does isPending help the user while the slow render catches up?
Say it in 60 seconds
Hard Technical round Senior Practice question

6. Clicking a tab that loads its data with Suspense replaces the whole screen with a spinner for a moment. How do you stop the page from disappearing, and where should the boundaries go?

What the interviewer is really testing:
Whether you understand how Suspense boundaries and transitions decide between keeping old content on screen and showing a fallback.
Answer frame:

Why it happens: the tab's content suspended and the nearest boundary is high up, so everything under it swaps to the fallback.

Transitions: when navigation runs as a transition, React keeps already-visible content on screen instead of hiding it, and isPending lets you show a light indicator.

Boundary placement: wrap regions that make sense to load on their own, so a fallback never replaces the navigation or the layout.

Sample spoken answer:

“When something suspends, React finds the nearest Suspense boundary above it and shows that fallback. If the only boundary sits near the root, the whole layout, header and tabs included, is replaced by a spinner, which feels like a crash. Two changes fix it. First, I make the tab switch a transition. When a transition makes content that's already on screen suspend, React keeps the old content visible and waits, instead of hiding it. I use isPending from useTransition to dim the old tab or show a thin progress bar. Second, I place boundaries on purpose: one around the tab panel, so when a panel does need a fallback, like on first load, the skeleton sits inside the layout, and separate ones around slow widgets that shouldn't hold up the rest. Boundaries are a decision about what the user sees while waiting, so I agree them with design rather than scattering them.”

Red flag to avoid:

Adding more spinners or one boundary around the whole app, without knowing a transition keeps visible content.

They may ask next:
  • What happens with a boundary that has never been shown before, even inside a transition?
  • How would you avoid a skeleton that flashes for only a few milliseconds?
Say it in 60 seconds

Effects and Refs 4 questions

Hard Technical round Senior Practice question

7. A ref callback written inline in the JSX attaches a ResizeObserver to a chart's container. The profiler shows the observer torn down and created again on every render. Why?

What the interviewer is really testing:
Whether you know how React treats a ref callback whose identity changes, and how to write one that attaches once.
Answer frame:

Identity: an inline arrow is a new function on every render, and React treats a new ref callback as a new ref.

What React does: after each commit it detaches the old callback by calling it with null, then calls the new one with the DOM node.

Fix: give the callback a stable identity with useCallback, or keep the node in a plain ref and set up the observer in an effect.

Sample spoken answer:

“React compares ref callbacks by identity. An inline arrow is a brand new function on every render, so after each commit React detaches the old one by calling it with null and then calls the new one with the same DOM node. If my callback creates a ResizeObserver when it gets a node and disconnects it when it gets null, the observer is destroyed and rebuilt on every render. On a chart that re-renders on hover, that's constant churn, and if the null branch forgets to disconnect, observers pile up. The fix is a stable callback: wrap it in useCallback with only the dependencies that should really re-attach it, and read changing values like an onResize prop from a ref. Or I keep a plain object ref and do the observing in an effect. In recent React versions a ref callback can also return a cleanup function, which React calls instead of passing null.”

Red flag to avoid:

Blaming ResizeObserver itself, or not knowing that a new ref callback means a detach and a re-attach.

They may ask next:
  • When is a ref callback a better choice than useRef plus an effect?
  • How would you measure an element whose DOM node can change, like a child that is rendered conditionally?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

8. An effect adds a scroll listener to containerRef.current, and its cleanup removes it using containerRef.current as well. When the component unmounts, the cleanup throws. Why, and what's the right pattern?

What the interviewer is really testing:
Whether you know refs are updated during the commit, before effect cleanups run, so a cleanup must not read ref.current again.
Answer frame:

Timing: React updates refs during the commit and sets them to null on unmount, before the effect cleanups run.

The bug: by cleanup time containerRef.current is already null, so the cleanup throws and the listener is never removed.

Fix: copy ref.current into a local variable inside the effect and use that same variable in the cleanup.

Sample spoken answer:

“The cleanup runs later than people think. React updates refs as part of the commit, and on unmount it sets the ref to null. Only after that do the effect cleanups run. So by the time my cleanup reads containerRef.current, it's null, and calling removeEventListener on it throws. The listener is never removed, so the handler and everything it closes over stay alive. The fix is simple: read the ref once inside the effect, keep it in a local variable, and use that variable in both the setup and the cleanup, so they always talk to the same node. The hooks lint rule warns about exactly this. One more trap: changing ref.current doesn't make the effect run again, so if the node can be swapped while the component stays mounted, I use a ref callback instead, or keep the node in state.”

Code:
useEffect(() => {
  const node = containerRef.current;
  node.addEventListener('scroll', onScroll);
  return () => node.removeEventListener('scroll', onScroll);
}, [onScroll]);
Red flag to avoid:

Adding a null check in the cleanup and calling it fixed, while the listener stays attached.

They may ask next:
  • Why doesn't a change to ref.current make the effect run again?
  • When would you use a ref callback here instead of an effect?
Say it in 60 seconds
Hard Technical round Senior Practice question

9. A parent creates an event bus in its useEffect, and a child subscribes to that bus in its own useEffect. On first mount the child crashes because the bus doesn't exist yet. Why?

What the interviewer is really testing:
Whether you know effects run from children up to parents, and that objects children depend on shouldn't be created in an effect.
Answer frame:

Order: after a commit, React runs the children's effects before their parents' effects.

The bug: the child's effect runs while the parent's effect, which creates the bus, hasn't run yet.

Fix: create the object during render with a lazy state initializer and pass it down, keeping only real side effects in the effect.

Sample spoken answer:

“Effects run after the commit, and React runs them from the inside out: children's effects first, then their parents'. So on the first mount the child's effect runs while the parent's effect, which creates the bus, hasn't happened yet, and the child finds nothing. Layout effects follow the same order. The fix is to stop creating something the children need inside an effect. Creating a plain object isn't a side effect, so the parent can create it during render with a lazy state initializer, useState(() => createBus()), and pass it down through context. It exists before any child renders, and it stays the same object across renders. The parent's effect then only does the real side effects, like connecting the bus to a socket, and cleans them up. The broader lesson I share with teams is to never rely on effect order between components; if the order matters, the design needs changing.”

Red flag to avoid:

Adding a setTimeout in the child to wait for the parent.

They may ask next:
  • Why is useState a safer holder for the bus than useMemo?
  • How would you make the child robust if the bus can arrive later?
Say it in 60 seconds
Hard Technical round Senior Practice question

10. A WebSocket effect should connect once but always call the latest onMessage prop. Adding onMessage to the dependencies reconnects on every render. How do you solve it, and what's the subtle part?

What the interviewer is really testing:
Whether you know the latest-value ref pattern and why the ref must be written after a commit, not during render.
Answer frame:

Split the concerns: the connection depends on the URL; the handler is a value you only need to read when a message arrives.

Latest ref: keep the handler in a ref, update it after each commit, and let the socket callback read ref.current.

Subtle part: never assign the ref during render, because a render can be thrown away.

Sample spoken answer:

“The connection and the handler change for different reasons. The socket should only reconnect when the URL changes, but the handler changes every render because the parent creates a new function each time. So I keep the handler in a ref. A layout effect with no dependency array writes the latest onMessage into the ref after each commit, and the socket effect depends only on the URL and calls ref.current when a message arrives. It always calls the newest handler without reconnecting. The subtle part is where the ref gets written. It's tempting to assign it in the render body, but a concurrent render can be discarded, so the ref could hold a handler from a render that never reached the screen. Writing it in an effect ties it to committed renders. I might also ask the parent to wrap onMessage in useCallback, but the hook shouldn't depend on that.”

Code:
function useSocket(url, onMessage) {
  const handlerRef = useRef(onMessage);
  useLayoutEffect(() => {
    handlerRef.current = onMessage;
  });
  useEffect(() => {
    const ws = new WebSocket(url);
    ws.onmessage = event => handlerRef.current(event);
    return () => ws.close();
  }, [url]);
}
Red flag to avoid:

Silencing the lint rule and leaving an empty-array effect that calls a stale handler.

They may ask next:
  • Why use a layout effect to update the ref rather than a plain effect?
  • How would you test that a re-render doesn't open a second connection?
Say it in 60 seconds

Performance 1 question

Medium Technical round Mid-level, Senior Practice question

11. A heavy Panel is wrapped in React.memo, yet the profiler shows it rendering every time its parent does. The parent passes a title string and some JSX between the Panel's tags. Why doesn't memo help?

What the interviewer is really testing:
Whether you know children is just a prop, that JSX makes new objects on every render, and the composition trick that avoids memo altogether.
Answer frame:

Children is a prop: JSX between the tags becomes props.children, a new element object on every parent render.

Shallow compare fails: memo compares props by reference, so the new children object always counts as a change.

Better fixes: move the changing state down so the parent stops re-rendering, or pass the content in from a component above that doesn't change.

Sample spoken answer:

“memo does a shallow comparison of props, and children is just another prop. Every time the parent renders, the JSX between the Panel's tags runs again and produces new element objects, so props.children is a new reference and memo sees a change every time. The title string compares fine; the children don't. I could memoise the children with useMemo in the parent, but that's fragile, since one inline handler inside breaks it again. The better fix is usually structural. If the parent re-renders because of some fast-changing state, like hover or a timer, I move that state into a small component that wraps the Panel and receives the content as children from above. When only that wrapper's state changes, the children it received are the same objects as last time, so React skips them without any memo at all. Memo is my last tool, not my first.”

Red flag to avoid:

Adding memo, useMemo and useCallback everywhere without knowing that children is a prop.

They may ask next:
  • Why is memoising the children with useMemo fragile in practice?
  • How do you confirm in the profiler why a component rendered?
Say it in 60 seconds

Server Rendering 3 questions

Hard Technical round Senior Practice question

12. You moved product pages to streaming server rendering with Suspense. First bytes arrive much sooner, but a product that doesn't exist now returns status 200 with an error message inside. Why, and how do you fix it?

What the interviewer is really testing:
Whether you know that with streaming the status code and headers go out with the first chunk, so not-found and redirect decisions must be made before streaming starts.
Answer frame:

Why: streaming sends the status and headers with the shell, the part outside the Suspense boundaries, and they can't change afterwards.

The bug: the product lookup sits inside a Suspense boundary, so the page learns the product is missing after the 200 has gone out.

Fix: run the lookups that decide status or redirects before rendering, or in the shell, and keep only secondary, slow parts inside boundaries.

Sample spoken answer:

“With streaming, the server sends the shell as soon as it's ready, meaning everything outside the Suspense boundaries, and the HTTP status and headers go out with that first chunk. Once they're sent, they can't change. Our product lookup was inside a boundary, so the server had already sent a 200 and the layout by the time it found out the product didn't exist. All it could do was stream an error message into the page, which crawlers treat as a soft 404. The rule I set afterwards is that anything that decides the response, like not found, a redirect or an auth check, runs before rendering starts, in the route's data loading or in the shell. Only the parts that can safely arrive late, like reviews or recommendations, sit inside Suspense boundaries. We lost a little of the early first byte on those pages, and that was the right trade.”

Red flag to avoid:

Thinking the server can still set a 404 after streaming has started.

They may ask next:
  • What happens to a redirect that is discovered after the shell has been sent?
  • How do you decide what belongs in the shell and what goes inside a boundary?
Say it in 60 seconds
Hard Technical round Senior Practice question

13. After moving to server rendering, some users get hydration mismatch errors, and a 'last updated' time shows one value and then flips. What causes mismatches, and how do you fix them properly?

What the interviewer is really testing:
Whether you know what hydration expects, the common sources of mismatch, and the honest fixes rather than hiding the warning.
Answer frame:

What hydration expects: the first client render must produce the same markup the server sent, so React can attach to it.

Usual causes: times and time zones, random values, checks for window or storage during render, locale formatting, invalid HTML nesting, and extensions editing the page.

Fixes: make the first render match and update after mount, use useId for generated ids, and keep suppressHydrationWarning for one unavoidable piece of text.

Sample spoken answer:

“Hydration assumes the first client render produces exactly what the server sent, so React can reuse the HTML and just attach events. A time is the classic break: the server formats it in its own time zone, the browser in the user's, and the text differs. Other causes are random ids, checking window or local storage during render, locale-dependent number formatting, and invalid nesting like a div inside a p, which the browser's parser rearranges before React sees it. Browser extensions that inject markup cause some too. When React finds a mismatch it may throw away the server HTML for that part and render it on the client, which costs time and causes the flip. My fix is to render something stable on the server, then show the user-specific value in an effect after mount. For ids I use useId. suppressHydrationWarning is only for one element's text that truly can't match, never a blanket fix.”

Red flag to avoid:

Spreading suppressHydrationWarning around, or branching the markup on typeof window during render.

They may ask next:
  • Why does invalid HTML nesting cause a mismatch even when the JSX is identical on both sides?
  • How would you catch mismatches before they reach users?
Say it in 60 seconds
Hard Technical round Senior Practice question

14. In an app with server functions, a deleteProject action marked 'use server' is protected only by hiding the Delete button from non-admins. Why is that a security hole, and what should the function do?

What the interviewer is really testing:
Whether you know a server function becomes a network endpoint anyone can call with any arguments, so it needs its own authentication, authorisation and validation.
Answer frame:

What it is: a function marked 'use server' is exposed as an endpoint the browser calls, and anyone can call it directly.

The hole: hiding the button is only UI; a user can replay the request with another project id.

Fix: inside the function, check who the user is, check they may delete that project, and validate every argument as untrusted input.

Sample spoken answer:

“It feels like calling a function, but underneath the framework turns it into an HTTP endpoint, and the button just sends a request to it. Anyone who can load the app can find that request in the network tab and send it again with a different project id. Hiding the button protects nothing. So I treat every server function like a public API route. Inside it, I read the session on the server, never trust a user id passed in as an argument, check that this user may delete this particular project, and validate the arguments, because the types in my code don't exist at runtime. I keep those checks in a shared data-access layer rather than in each action, so a new action can't forget them, and I add a test that calls the action as a normal user and expects a refusal.”

Red flag to avoid:

Believing a server function is private because it lives in server code or is only shown to admins.

They may ask next:
  • What input validation would you add beyond checking the project id?
  • How would you stop a teammate from adding a new action that skips the checks?
Say it in 60 seconds

Reliability 1 question

Hard Technical round Senior Practice question

15. Right after every deploy, error tracking fills with failures to load dynamically imported chunks, mostly from users who already had the app open. What's going on, and how do you design it away?

What the interviewer is really testing:
Whether you think about the gap between deploys and long-lived tabs: hashed chunk names, old assets being deleted, and graceful recovery.
Answer frame:

Cause: an open tab still runs the old build, and its lazy imports ask for old hashed chunk files that the new deploy removed.

Prevent: keep previous builds' assets available for a while instead of wiping them on every deploy.

Recover: catch chunk load failures in an error boundary around lazy routes, reload once to get the new build, and guard against reload loops.

Sample spoken answer:

“Lazy-loaded routes are separate files with a content hash in the name. A user who opened the app before the deploy is still running the old build, so when they click to a new route, their code asks for an old chunk file. If the deploy replaced the whole assets folder, that file is gone and the import fails. Errors spike after every release and fade as tabs get refreshed. First I'd stop deleting old assets on deploy and keep a few previous builds' files on the server or CDN, which fixes most of it. Then I'd handle the rest: an error boundary around lazy routes that recognises a chunk load failure and reloads the page once, with a flag in session storage so it can't loop. For tabs that stay open for days, I'd have the app check for a new version now and then and offer a refresh at a safe moment, like a route change.”

Red flag to avoid:

Retrying the import in a loop, or telling users to clear their cache.

They may ask next:
  • Why can a CDN serving old HTML make this worse?
  • How would you avoid losing a half-filled form when you force a reload?
Say it in 60 seconds

Architecture 2 questions

Hard System design round Senior Practice question

16. Five teams share one large React app and keep blocking each other's releases. Someone proposes micro-frontends. How do you decide, and if you go ahead, what do you standardise?

What the interviewer is really testing:
Whether you treat micro-frontends as an organizational trade-off with real runtime costs, and know what must stay shared when you split.
Answer frame:

Name the pain: release coupling, slow builds or unclear ownership each have cheaper fixes than a runtime split.

Count the cost: duplicate dependencies, one React instance to share, screens drifting apart, cross-app state and harder performance work.

If you split: split by route or business area, one shell owns routing, login and the design system, shared libraries are singletons with a version policy, and teams talk through the URL and events.

Sample spoken answer:

“I'd start with what hurts. If it's one release train where any team's bug blocks everyone, a monorepo with clear module boundaries, owners per folder and independent test runs might fix it without splitting the runtime. Micro-frontends buy independent deployment, but they cost a lot: extra copies of libraries if you're careless, React that must be shared as one instance, screens that drift apart visually, and performance that nobody owns. If teams really need their own release schedule, I'd split along routes or business areas, never widgets on the same page. One shell app owns routing, login, error handling and the design system. React and the design system are shared singletons with a policy for upgrading them. Teams talk through the URL and a few documented events, not shared global state. And I'd set a performance budget per area, because total page weight is where these setups quietly fail.”

Red flag to avoid:

Choosing micro-frontends because they sound scalable, without naming the problem or the runtime cost.

They may ask next:
  • How would you upgrade React across all the micro-frontends without a big-bang release?
  • What would make you merge them back into one app?
Say it in 60 seconds
Hard System design round Senior Practice question

17. A product has marketing pages, a catalogue with hundreds of thousands of item pages, and a logged-in dashboard. How would you choose between static, server-rendered, streamed and client-only rendering for each part?

What the interviewer is really testing:
Whether you pick rendering strategies from each area's freshness, search and personalisation needs instead of one choice for the whole product.
Answer frame:

Marketing: rarely changes and must be fast and crawlable, so build it static and serve it from the CDN.

Catalogue: too many pages to build ahead, so render on request behind a cache with revalidation, and stream slow parts like reviews.

Dashboard: personal, interactive and not for search, so a client-heavy app behind login, maybe with a server-rendered shell.

Across all: one design system and data layer, with written rules for what can be cached and for how long.

Sample spoken answer:

“I'd decide per area, from three questions: how fresh does the data need to be, does search need it, and how personal is it. Marketing pages change rarely and must be fast and crawlable, so I'd build them static and serve them from the CDN. The catalogue has too many pages to rebuild on every change, and prices and stock move, so I'd render on request with a cache in front and a short revalidation window, and stream the slower, less important parts like reviews and recommendations so the product details arrive first. Price and stock I'd keep fresh, possibly fetched on their own. The dashboard is personal and not for search, so a client-heavy app behind login is fine, maybe with a server-rendered shell for a quicker first paint. What I'd standardise across all three is the design system, the data layer and the caching rules, so a price never shows two values on two pages.”

Red flag to avoid:

Picking one rendering mode for the whole product, or server-rendering the logged-in dashboard just because it's the trend.

They may ask next:
  • What goes wrong if personalised content ends up in a shared cache?
  • How would you refresh catalogue pages quickly when a price changes?
Say it in 60 seconds

Leadership 5 questions

Medium Situational round Senior Practice question

18. You're asked to set front-end standards for a dozen teams whose React codebases have drifted apart. What do you standardise, what do you leave to teams, and how do you make it stick?

What the interviewer is really testing:
Whether you can set standards teams actually adopt: few of them, enforced by tools, decided together, with room for local choice.
Answer frame:

Pick few: standardise what crosses team lines or hurts users when it varies: design system, accessibility, performance budgets, error reporting, security rules.

Leave local: folder layout, small library choices and style that only affects one team.

Make it stick: put rules in shared configs, templates and CI checks, decide them with a rep from each team, and write down why.

Sample spoken answer:

“I'd look before deciding. I'd read a few codebases, gather incidents and pain points, and ask each team what slows them down. Then I'd standardise only what crosses team lines or hurts users when it varies: the design system, accessibility rules, a performance budget per page, error reporting, dependency and security policy, and the hooks lint rules. Everything else, like folder layout or small library choices, stays with the teams. To make it stick, rules live in tools rather than wiki pages: a shared lint and TypeScript config, a project template, and CI checks that fail a build on budget or accessibility regressions. Decisions go through a small group with one engineer from each team, written up with the reasoning, so it isn't me handing rules down. And I'd keep an exception process, because a standard with no way out gets ignored quietly.”

Red flag to avoid:

Writing a long style guide nobody enforces, or standardising everything down to folder names.

They may ask next:
  • A strong team refuses to adopt the shared design system. What do you do?
  • How do you retire a standard that no longer makes sense?
Say it in 60 seconds
Hard Behavioral round Senior Practice question

19. Tell me about a time you asked leadership to put roadmap time into front-end performance or reliability instead of features. How did you make the case, and what happened?

What the interviewer is really testing:
Whether you can turn an engineering concern into business terms, back it with your own product's data, and accept a trade-off.
Answer frame:

The problem: what you saw, and why it mattered to users and the business.

The case: data from your own product, a scoped proposal, and the feature it would cost.

The result: what was agreed, what shipped, and how you showed it worked.

Sample spoken answer:

“At my last company our main product page had become slow on mid-range phones, but nobody felt it on office laptops. I pulled our own field data, split by device, and lined it up with the funnel. Users on slower phones dropped off at that page far more often than users on fast ones. I didn't ask for a vague quarter of performance work. I proposed four weeks for one team with three concrete fixes: split the bundle for that route, stop loading the reviews widget up front, and take a heavy date library off the main path. I also named which feature would slip. Leadership agreed, on the condition that we showed the result. Load and interaction times dropped clearly on slower phones, and the drop-off there shrank. The cost was a delayed feature, and I added a performance budget to CI so we wouldn't need another rescue.”

Red flag to avoid:

Asking for time to clean up code with no link to users or the business.

They may ask next:
  • What would you have done if leadership had said no?
  • How did you show the improvement came from your changes and not from something else?
Say it in 60 seconds
Hard Behavioral round Senior Practice question

20. Walk me through a front-end project you led that went badly or got cancelled. Where did your own judgement fail, and what do you do differently now?

What the interviewer is really testing:
Whether you own a failure honestly, can name your specific mistake, and changed how you work because of it.
Answer frame:

What happened: the goal, your role, and how it went wrong.

Your mistake: one specific decision you'd make differently, not bad luck.

What changed: the habit or process you use now.

Sample spoken answer:

“I led building an in-house form engine. The idea was that product teams would describe forms in JSON and the engine would render them, with validation and layout built in. It worked well for the first few simple forms, so more teams adopted it. Then real requirements arrived: fields that depend on each other, custom widgets, unusual layouts. Each one added another option to the schema, and within a year the JSON had become a programming language nobody could debug, and teams waited on my team for every change. We eventually froze it and let teams build forms in plain React with shared field components. My mistake was designing a platform from three easy cases. I never tested it against the hardest form in the product, and I gave teams no escape hatch. Now I prove any abstraction on the ugliest real case first, and I always leave a way to drop down to plain code.”

Red flag to avoid:

Blaming other teams or shifting priorities, with no mistake of your own.

They may ask next:
  • How did you explain the change of direction to the teams already using it?
  • Looking back, should the engine have been built at all?
Say it in 60 seconds
Medium Culture fit round Senior Practice question

21. Your team keeps shipping bugs from effects and stale state, and code review isn't catching them. As the senior person, what did you do, or what would you do, to raise the team's React skill?

What the interviewer is really testing:
Whether you grow a team through systems rather than heroics: guardrails, shared tools, teaching and review habits.
Answer frame:

Find the pattern: group recent bugs to see which mistakes repeat.

Guardrails: enforce the hooks lint rules, add shared hooks for the common cases, and set clear review expectations.

Teaching: short sessions built on the team's own bugs, and pairing on the tricky ones.

Sample spoken answer:

“At my last company I grouped three months of front-end bugs and found most came from a handful of patterns: effects used to sync derived state, missing dependencies silenced with comments, and fetches with no cancellation. So I did three things. First, guardrails: we made the hooks lint rules errors instead of warnings, and any disable comment needed a written reason that reviewers could challenge. Second, shared tools: one data-fetching hook that handled cancellation and races, so nobody wrote that by hand again. Third, teaching from our own code: a thirty-minute session every two weeks walking through one real bug and its fix, and I paired with the two people newest to hooks. Within a couple of months that class of bug mostly dropped off our incident list. The cost was slower reviews for a few weeks, which the team accepted once they saw why.”

Red flag to avoid:

Saying you'd simply review every pull request yourself.

They may ask next:
  • How do you handle a senior teammate who thinks the lint rules are too strict?
  • How did you know it had actually worked?
Say it in 60 seconds
Medium Culture fit round Senior Practice question

22. When you interview a senior React engineer, what do you ask to tell real depth apart from someone who only knows the vocabulary?

What the interviewer is really testing:
Whether you know what senior React skill looks like in practice, and can assess it fairly and consistently.
Answer frame:

Real code: a short, realistic component with a bug and a performance issue, instead of trivia.

Probe decisions: ask why they chose an approach and what changes when the screen grows.

Signals: trade-offs named without prompting, knowing when not to use a tool, and saying what they'd measure.

Sample spoken answer:

“I avoid definition questions, because anyone can recite what useMemo does. I give a small, realistic component, maybe a list with filtering and a fetch, that has a race condition and a needless re-render, and ask them to review it as if it were a teammate's pull request. Strong seniors find the bug that matters first, explain why it happens in terms of how React renders, and suggest a fix that fits the codebase rather than the cleverest one. Then I push on decisions: where would state live if this screen grew, when would you not memoise, what would you measure before optimising. The signals I listen for are trade-offs named without prompting, comfort saying they'd check something, and stories with real mistakes in them. I score against a written rubric agreed with the other interviewers, so the decision isn't just gut feel.”

Red flag to avoid:

Hiring on trivia recall, or on whether the candidate solves it the way you would.

They may ask next:
  • How do you keep the loop fair for someone who freezes in live coding?
  • What would make you say no to a very strong coder?
Say it in 60 seconds

Migrations 1 question

Hard Situational round Senior Practice question

23. Your product has a large front end written in an older JavaScript framework, and you're asked to move it to React without freezing features. How do you run the migration?

What the interviewer is really testing:
Whether you can run an incremental migration across a big codebase, with a bridge between two runtimes and a real finish line.
Answer frame:

Replace piece by piece: new features in React from day one, old screens moved route by route or as islands inside old pages.

Bridge: mount React roots inside the old app, share data and events through a small adapter, and share design tokens so both sides look the same.

Finish: block new code in the old framework, track what's left, and set a date when both runtimes stop shipping together.

Sample spoken answer:

“I'd never do a big rewrite in a branch. I'd run the two side by side and move piece by piece. First, a rule: every new feature is built in React. Then the bridge. React can mount into any DOM node with createRoot, so I can drop React islands into old pages, and whole routes move over once most of a screen is React. The two sides share data through a small adapter, like a store both can read or a few agreed events, and share design tokens so users don't see two styles. I'd order screens by value and risk, move the ones being changed anyway first, and tackle one of the ugliest early to find the real problems. A CI rule blocks new code in the old framework. And I'd track what's left and set a date to remove the old runtime, because shipping two frameworks costs bundle size every month it drags on.”

Red flag to avoid:

Proposing a full rewrite with a feature freeze, or a migration with no end date.

They may ask next:
  • How do you handle a React island that needs to navigate inside the old router?
  • What would you do if the migration stalls with a third of the screens left?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

You practiced these. On the real call, ClapAssist helps with the rest.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.

Download with 10 free minutes
Mac and Windows · Stays out of screen share · No card