Bugs You Traced • State in Real Screens • Effects • Data Fetching • Testing • TypeScript • 2026

React Interview Questions for 3 Years Experience (2 to 4 Years)

React interviews for three years of experience (two to four years) ask less about definitions and more about what happened in your code: a form that kept the last user's values, a chat that scrolled one message short, a double order from a double click, a crash that only showed up in the production build. It is written for React developers with about two to four years of real work, the stage where you own screens and features end to end but someone else still designs the whole system. Each question shows what the interviewer is checking, the shape of a good answer and a short spoken answer. Swap the stories for your own before the interview.

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

Rendering 1 question

Hard Technical round Mid-level Practice question

1. In a chat screen, after sending a message you call setMessages and then scroll to the last message, but it always stops one message short. Why, and how did you fix it?

What the interviewer is really testing:
Whether you know a state update doesn't change the DOM straight away, and know where to run code that needs the updated DOM.
Answer frame:

Cause: setMessages only schedules a render, so on the next line the DOM still holds the old list and the last item is the previous message.

Fix one: scroll from an effect that runs when the messages change, after React has updated the DOM.

Fix two: wrap the update in flushSync when the scroll must happen right there in the handler, and use it sparingly.

Sample spoken answer:

“Our send handler added the message with setMessages and on the very next line scrolled the list's last child into view. But setting state doesn't touch the DOM immediately. It asks React to render, and that happens after the handler finishes. So at the moment we scrolled, the new message wasn't in the DOM yet, and the last child was the previous message. That's why it always stopped one short. I moved the scrolling into an effect that runs when the message count changes, with a ref on an empty div at the bottom of the list, so it runs after React has put the new message on the page. That also covered incoming messages, which we wanted anyway. React also has flushSync, which applies the update right away so the DOM is current on the next line, but it forces an extra synchronous render, so I keep it for rare cases.”

Code:
const bottomRef = useRef(null);

useEffect(() => {
  bottomRef.current?.scrollIntoView({ block: 'end' });
}, [messages.length]);

// at the end of the message list
<div ref={bottomRef} />
Red flag to avoid:

Wrapping the scroll in a setTimeout until it seems to work, without knowing why the DOM was behind.

They may ask next:
  • Should the list still jump to the bottom if the user has scrolled up to read older messages?
  • Would useLayoutEffect be a better fit here, and why?
Say it in 60 seconds

State 4 questions

Medium Technical round Mid-level Practice question

2. On a page with a user list and an edit panel, picking a different user still showed the previous user's half-typed values in the form. How did you fix it?

What the interviewer is really testing:
Whether you know React keeps state tied to position in the tree, and that a key is the clean way to reset a component.
Answer frame:

Cause: the form stays at the same place in the tree with the same type, so React keeps its state even though the user prop changed.

Fix: give the form a key of the user's id, so a new user means a fresh component with fresh state.

Avoid: an effect that copies the new user into state, which renders stale values first and is easy to get wrong.

Sample spoken answer:

“The edit form kept its fields in local state, started from the user prop. When you clicked another user, the same EditForm component stayed in the same spot in the tree, so React treated it as the same instance and kept its state. The new user prop arrived, but useState had already used its initial value. My first idea was an effect that reset the fields when the id changed, but that renders one frame of old data and gets messy with more fields. The cleaner fix was putting key equal to the user's id on the form. When the id changes, React unmounts the old form and mounts a new one, so every field starts from the new user. It also reset the validation errors and the dirty flag for free.”

Red flag to avoid:

Adding a useEffect that calls five setters when the id changes, and never mentioning key as a reset.

They may ask next:
  • When would resetting with a key be too expensive?
  • How would you warn the user about unsaved changes before switching?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

3. A component did useState(props.price), and when the parent later sent a new price the screen kept showing the old one. Why, and what did you change?

What the interviewer is really testing:
Whether you know the useState argument is only read on the first render, and prefer deriving values over mirroring props.
Answer frame:

Cause: the argument to useState is the initial value only; later renders ignore it.

Best fix: don't copy the prop at all; read it directly or compute what you need during render.

When a copy is real: if the user edits a draft, name the prop initialPrice so everyone can see it only seeds the draft.

Sample spoken answer:

“The component copied props.price into state so it could format it, and the price came from a parent that refreshed every minute. useState only reads its argument on the first render, so the state kept the first price forever while the prop moved on. There was no reason for state there at all, because nothing inside the component changed the price. I deleted the state and formatted props.price directly during render, so it's always in sync. Where a copy really was needed, like an edit box where the user types a new price, I renamed the prop to initialPrice to make it clear it's only a starting point. I try not to keep two copies of one value.”

Red flag to avoid:

Adding a useEffect that sets state whenever the prop changes and calling it done, without asking why the state exists.

They may ask next:
  • How would you handle a form where the user edits but the server can also update the value?
  • Why is syncing a prop into state with useEffect a weak fix?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

4. A checkout component had seven useState calls that always changed together, and a bug left it showing both a success and an error message. How did you clean it up?

What the interviewer is really testing:
Whether you can spot impossible states and use a reducer or a single status value to make them impossible.
Answer frame:

Problem: separate flags like isLoading, isError and isSuccess can drift into combinations that should never happen.

Fix: one status value with named cases, and a reducer that moves between them on clear actions.

Bonus: transitions live in one pure function you can unit test without rendering.

Sample spoken answer:

“The checkout had separate state for loading, error, success, the order id, the error message and a couple more. Each handler set some of them, and one path set success without clearing the old error, so both banners showed. I replaced the flags with a single status that could be idle, submitting, succeeded or failed, and moved the updates into useReducer. Actions like submit, succeeded and failed each return a complete new state, so an error and a success can't exist together any more. The reducer is a plain function, so I wrote a few quick unit tests for the transitions without rendering anything. The component got shorter too, because the handlers just dispatch an action. I don't reach for a reducer for every form, only when several values change as one.”

Code:
function checkoutReducer(state, action) {
  switch (action.type) {
    case 'submit':
      return { status: 'submitting' };
    case 'succeeded':
      return { status: 'succeeded', orderId: action.orderId };
    case 'failed':
      return { status: 'failed', message: action.message };
    default:
      return state;
  }
}

const [state, dispatch] = useReducer(checkoutReducer, { status: 'idle' });
Red flag to avoid:

Fixing the one path that forgot to clear the error and leaving seven flags that can still disagree.

They may ask next:
  • When is useReducer overkill compared with a couple of useState calls?
  • Why must a reducer stay pure, and what goes wrong if it calls an API?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

5. Users complained that the filters on an orders list were lost on refresh, and they couldn't send a filtered view to a colleague. How did you rebuild it?

What the interviewer is really testing:
Whether you see the URL as state, and handle the details: parsing, defaults, history entries and typing into a search field.
Answer frame:

Move it: filters, sort and page go into the URL search params instead of component state.

Read and write: parse the params into typed values with defaults, and update them through the router.

Details: replace the history entry while typing so Back still works, and reset the page number when a filter changes.

Sample spoken answer:

“The filters lived in useState, so a refresh wiped them and a link always opened the default view. I moved status, date range, sort and page into the URL search params using the router's search params hook. The component reads the params, turns them into proper values with defaults, and the fetch depends on those values, so a shared link loads exactly the same view. When a filter changes I write it back to the URL and reset the page to one, because page five of a new filter often doesn't exist. For the search box I debounced the update and replaced the history entry instead of pushing one, otherwise every letter became a Back step. I also left defaults out of the URL so links stayed short and readable.”

Red flag to avoid:

Saving the filters in localStorage only, which survives a refresh but still can't be shared.

They may ask next:
  • What should happen if someone edits the URL by hand to an invalid value?
  • Which state would you never put in the URL?
Say it in 60 seconds

Effects 3 questions

Medium Technical round Mid-level Practice question

6. In development, a chat screen opened two WebSocket connections and every message showed up twice. Production looked fine. What was going on?

What the interviewer is really testing:
Whether you know Strict Mode runs effects twice in development on purpose, and that the real bug is a missing cleanup.
Answer frame:

Why twice: in development, Strict Mode mounts, unmounts and mounts again to show effects that don't clean up.

Real bug: the effect opened a socket and never closed it, so the first one stayed alive.

Fix: return a cleanup that closes the socket and removes listeners; don't switch Strict Mode off.

Sample spoken answer:

“Strict Mode in development deliberately runs each effect, then its cleanup, then the effect again, as if the component mounted twice. Our effect opened a WebSocket and added a message listener but returned nothing, so the first connection never closed and both pushed messages into state. Production only runs it once, which hid the bug, but it would have shown up there too when a user left the chat and came back, or when the room id changed. The fix was returning a cleanup that removes the listener and closes the socket. After that, development showed one connect, one close, one connect, and a single live connection. A teammate had suggested turning Strict Mode off, but that just hides leaks like this one.”

Red flag to avoid:

Removing Strict Mode or adding a ref flag to skip the second run, without adding the missing cleanup.

They may ask next:
  • What would you do if the connection was expensive and the double connect in development bothered the team?
  • Where else in your code would a missing cleanup show up in production?
Say it in 60 seconds
Hard Technical round Mid-level Practice question

7. A tooltip that measures its own size to stay on screen flickered at the wrong position for a frame before jumping into place. How did you fix it?

What the interviewer is really testing:
Whether you know when useEffect runs compared with the browser paint, and when useLayoutEffect is the right, rare choice.
Answer frame:

Cause: useEffect usually runs after the browser has painted, so the first position shows before the corrected one.

Fix: measure and set the position in useLayoutEffect, which runs after the DOM update but before paint.

Care: it blocks painting, so keep the work small, and it doesn't run during server rendering.

Sample spoken answer:

“The tooltip rendered at a default spot, then an effect measured it with getBoundingClientRect and moved it if it went off screen. Because useEffect usually runs after the browser has painted, users saw one frame in the wrong place and then a jump. I moved the measuring and the position update into useLayoutEffect. That runs after React has updated the DOM but before the browser paints, and a state update inside it is handled before the paint too, so the user only ever sees the final position. I kept it to that one measurement, because anything slow in a layout effect delays the whole frame. It's the only place in that app we used it, and on the server-rendered pages the tooltip only opens after a hover, so the server never needed to run it.”

Red flag to avoid:

Hiding the flicker with a setTimeout or a CSS fade, without knowing why the first paint was wrong.

They may ask next:
  • Why shouldn't useLayoutEffect be your default for all effects?
  • How would you keep the tooltip in place when the page scrolls?
Say it in 60 seconds
Hard Technical round Mid-level Practice question

8. You wrote a useOnlineStatus hook with useState and an effect that listens to online and offline events. A reviewer suggested useSyncExternalStore instead. What does that change?

What the interviewer is really testing:
Whether you know the hook React provides for reading an outside data source, and why it beats copying that source into state with an effect.
Answer frame:

Old way: state starts from a guess, then an effect subscribes and corrects it, so the first render can be wrong.

New way: pass a subscribe function and a getSnapshot function; React reads the current value during render and re-renders when it changes.

Details: getSnapshot must return the same value when nothing changed, and an optional third function gives the value during server rendering.

Sample spoken answer:

“My version started with useState set to true, then an effect added online and offline listeners and updated the state. It worked, but the first render used a guess instead of the real value, and it's exactly the case React has a hook for. With useSyncExternalStore I pass a subscribe function that adds both listeners and returns a function that removes them, and a getSnapshot function that returns navigator.onLine. React reads the real value during render and keeps every component showing the same value, even during concurrent rendering. It resubscribes whenever it gets a new subscribe function, so I define that outside the component. For server rendering I pass a third function that returns true. The one trap is getSnapshot: it must return the same value if nothing changed, so building a new object on every call causes endless re-renders.”

Code:
function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine,
    () => true
  );
}
Red flag to avoid:

Saying the effect version is exactly the same, or passing a new subscribe function on every render without knowing it resubscribes each time.

They may ask next:
  • Why would returning a new object from getSnapshot cause an endless loop?
  • Which other browser or library values would you read this way?
Say it in 60 seconds

Data Fetching 2 questions

Medium Technical round Mid-level Practice question

9. After a user saved an edit on a detail page and went back, the list still showed the old value until a full reload. The app used a data-fetching library with a cache. What did you do?

What the interviewer is really testing:
Whether you understand client-side server caches, stale data and invalidation after a change.
Answer frame:

Cause: the list's cached result was still treated as fresh, so going back showed the cache without refetching.

Fix: after the save succeeds, invalidate the list's cache key so it refetches, or update the cached item directly.

Keys: consistent cache keys per resource make invalidation simple and reliable.

Sample spoken answer:

“We used TanStack Query, and the list query had a stale time set so it wouldn't refetch on every visit. The save went through a mutation, but nothing told the cache that the list was now out of date, so going back showed the cached copy. I added an onSuccess to the mutation that invalidates the orders list key and sets the updated order into the detail key. Invalidation marks the list stale, and because it was on screen or about to be, it refetched right away. I also cleaned up our keys so every orders query started with the same prefix, which let one call invalidate all the filtered versions of the list. For small edits where the server returns the updated item, updating the cache directly saved a network round trip.”

Red flag to avoid:

Setting stale time to zero everywhere or forcing a page reload after every save.

They may ask next:
  • When would you update the cache directly instead of invalidating it?
  • What does stale time control compared with how long unused data stays in the cache?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

10. Support found a few customers with two identical orders placed a second apart. The place-order button was a normal button with an async click handler. How did you stop it?

What the interviewer is really testing:
Whether you guard an action that must not repeat on both the client and the server, instead of trusting a disabled button alone.
Answer frame:

Cause: nothing stopped a second click or a second Enter while the first request was still in flight.

Client: keep a submitting state, disable the button and show progress, and ignore submits while it's true.

Server: send an idempotency key made once per checkout attempt, so a repeated request returns the first order instead of creating another.

Sample spoken answer:

“The handler awaited the request, but nothing changed on screen until it came back, so impatient users clicked again, and a slow network made the gap long enough to place two orders. On the client I added a submitting state, set before the request and cleared in finally, disabled the button while it was true and changed its label to show progress. I also made the form's submit handler return early while submitting, because pressing Enter in a field can submit too. But the client can't be the only guard, since a retry or a flaky connection can still repeat a request. So we generated an idempotency key when the checkout page opened and sent it with the order, and the backend returned the existing order if it saw the same key again. Both halves together closed it.”

Red flag to avoid:

Only disabling the button and assuming the problem is gone, with nothing on the server side.

They may ask next:
  • Why can't you rely on the disabled button alone?
  • When should a new idempotency key be made, and what goes wrong if one is reused for too long?
Say it in 60 seconds

Debugging 2 questions

Medium Technical round Mid-level Practice question

11. A page works perfectly on your machine, but in production it crashes with a short 'Minified React error' and a number. How did you track down what was actually wrong?

What the interviewer is really testing:
Whether you can debug a failure that only happens in production: decode the error, map the stack to your code and reproduce the production build locally.
Answer frame:

Decode: the message links to the full error text on the React website; read that first.

Map: use source maps, in the error tracker or the browser, to turn the minified stack into your files and lines.

Reproduce: build and serve the production bundle locally against the same data, since development and production can behave differently.

Sample spoken answer:

“The production build strips React's long error messages to keep the bundle small, so you get a number and a link. The link opens the full message, and ours said objects are not valid as a React child. That told me some component was rendering an object instead of text. Our error tracker had source maps uploaded, so the stack pointed at a price label in the order summary. I built the app for production, served it locally against the same environment, and it crashed the same way. The cause was data, not the build: one production record had the price as an object with an amount and a currency, while our test data always had a plain number. I fixed the rendering, added a guard and a test with that shape, and raised the mismatch with the backend team.”

Red flag to avoid:

Saying you can't debug it until it happens in development, or ignoring the link in the error message.

They may ask next:
  • Why upload source maps to your error tracker rather than serve them to every visitor?
  • What else can behave differently between a development and a production build?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

12. A price on screen is wrong and you don't know which component passed it down. Walk me through how you use React DevTools to find the source.

What the interviewer is really testing:
Whether you use the Components tab for real work, not just console logs, and can trace a value up the tree.
Answer frame:

Pick: use the element picker to select the wrong price and land on its component.

Trace: read its props, then walk up the owners list to see who rendered it and where the value changes.

Confirm: edit a prop or state value live, and jump to the component's source to set a breakpoint.

Sample spoken answer:

“I open the Components tab and use the picker to click the wrong price, which selects the component that rendered it. On the right I can see its props, state and hooks. If the price is already wrong in its props, I look at the rendered-by list, which shows the chain of owners, and click up it until I find the component where the value first goes wrong. Last time that was a parent that passed the price before a discount was applied, instead of after. I can edit props and state live to check a theory without changing code, and jump to the source file to drop a breakpoint in the function that computes the value. The search box helps when the tree is huge. It's usually faster than adding logs to five files.”

Red flag to avoid:

Only adding console.log calls everywhere, or not knowing DevTools can show props and state at all.

They may ask next:
  • How do you check which props changed between two renders?
  • What do you do when DevTools shows a component name like Anonymous?
Say it in 60 seconds

Testing 2 questions

Medium Technical round Mid-level Practice question

13. Your component tests mocked the API client module directly, and they broke every time someone changed how data was fetched. How did you change the way you mock API calls?

What the interviewer is really testing:
Whether you mock at the network edge so tests check behaviour, and can set up error and empty cases cleanly.
Answer frame:

Problem: mocking the client module ties each test to how the code fetches, not to what the user sees.

Fix: intercept requests at the network level with shared handlers for the happy path, and render the real component with its real hooks.

Per test: override one handler for an error, empty or slow response, and reset the handlers after each test.

Sample spoken answer:

“Every test mocked our api module function by function, so when we moved a screen to a data-fetching library or renamed a function, dozens of tests failed even though the screen worked. I switched us to Mock Service Worker, which intercepts the actual HTTP requests in tests. We kept a shared set of handlers returning realistic data for the common endpoints, started the mock server before the tests and reset the handlers after each one. Now a test renders the real component with the real hooks and the real client, and only the network is fake. For an error case, the test overrides one handler to return a 500, then checks the error message and the retry button. The next refactor of our data layer changed no tests at all, which was the whole point.”

Red flag to avoid:

Only asserting that a mocked function was called with certain arguments, and never checking what the user sees.

They may ask next:
  • How do you make a test fail if the component calls an endpoint you didn't expect?
  • When is mocking a module still the right choice?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

14. You pulled paging logic out of a table into a custom hook. Show me how you tested the hook on its own.

What the interviewer is really testing:
Whether you can test a hook's behaviour through renderHook and act, including edges, without rendering the whole table.
Answer frame:

Render it: use renderHook to run the hook inside a tiny test component.

Act: call its functions inside act so state updates are applied before you assert.

Edges: test the limits and what happens when the inputs change, using rerender with new props.

Sample spoken answer:

“The hook took a page count and returned the current page plus next and prev functions that stop at the ends. I tested it with renderHook from Testing Library, which renders the hook inside a small test component and gives me result.current. I call next inside act, so React applies the update before I check the value. The useful tests were the edges: going past the last page stays on the last page, prev on page one stays on one. I also used rerender with a smaller page count to check the current page clamps down when the data shrinks, which was a real bug in the old table. The table itself then only needed one test showing the buttons are wired up, instead of every edge case through clicks.”

Code:
import { renderHook, act } from '@testing-library/react';
import { usePagination } from './usePagination';

test('stops at the last page and clamps when data shrinks', () => {
  const { result, rerender } = renderHook(
    ({ pageCount }) => usePagination({ pageCount }),
    { initialProps: { pageCount: 3 } }
  );
  act(() => result.current.next());
  act(() => result.current.next());
  act(() => result.current.next());
  expect(result.current.page).toBe(3);

  rerender({ pageCount: 2 });
  expect(result.current.page).toBe(2);
});
Red flag to avoid:

Testing internal details like how many times setState was called, instead of what the hook returns.

They may ask next:
  • When would you test the hook only through the component that uses it?
  • How would you test a hook that calls fetch?
Say it in 60 seconds

TypeScript 1 question

Medium Coding round Mid-level Practice question

15. Write a typed Button component for your design system that takes a variant and a loading flag but still accepts every normal button attribute.

What the interviewer is really testing:
Whether you can type React components properly in TypeScript, extend native props and pick safe defaults.
Answer frame:

Extend native props: start from the built-in button props so onClick, aria attributes and the rest just work.

Own props: add a union type for variant and a boolean for loading.

Safe defaults: default type to button, and disable the button while loading.

Sample spoken answer:

“I start from React.ComponentPropsWithoutRef for a button, so every normal attribute is typed and allowed, and intersect it with my own props: a variant limited to a small union and an optional loading flag. In the component I pull out my props and spread the rest onto the real button. Two defaults matter. Type defaults to button, because a plain button inside a form submits it, which caused a real bug for us once. And while loading, the button is disabled and marked busy, so a second click can't send the request again. The union type means someone typing variant equals danger gets an error in the editor instead of a silently unstyled button. If the team needs refs, I'd add ref support too.”

Code:
type ButtonProps = React.ComponentPropsWithoutRef<'button'> & {
  variant?: 'primary' | 'secondary';
  loading?: boolean;
};

export function Button({
  variant = 'primary',
  loading = false,
  type = 'button',
  disabled,
  children,
  ...rest
}: ButtonProps) {
  return (
    <button {...rest} type={type} data-variant={variant}
      disabled={disabled || loading} aria-busy={loading}>
      {children}
    </button>
  );
}
Red flag to avoid:

Typing props as any, or re-declaring onClick, disabled and every other attribute by hand.

They may ask next:
  • How would you let this Button render as a link while keeping the types correct?
  • How do refs reach the inner button in React 19 compared with older versions?
Say it in 60 seconds

Components 3 questions

Medium Technical round Mid-level Practice question

16. A modal you built was cut off and sat behind other content when it opened from inside a card with overflow hidden. How did you fix it, and what else did the modal need?

What the interviewer is really testing:
Whether you know portals, stacking contexts, and the behaviour a usable modal must have beyond showing up.
Answer frame:

Cause: the card clipped overflow and had a transform, which traps a fixed-position child inside it and creates its own stacking context, so z-index couldn't escape.

Fix: render it through createPortal into document.body, or use the native dialog element with showModal.

Behaviour: close on Escape, move focus in and keep it there, and return focus to the button that opened it.

Sample spoken answer:

“The modal was rendered inside a card that had overflow hidden and a transform. A transform makes the card the box that a fixed-position child is placed in, so the overlay was sized to the card and clipped by it, and it also creates a new stacking context, so no z-index could lift it above the rest of the page. I moved the modal into a portal with createPortal targeting document.body, which puts it at the top of the DOM while it stays in the same place in the React tree, so context and events still flow from its React parents. Then I fixed what a modal needs: Escape and the close button close it, focus moves into it on open and stays inside while tabbing, the page behind stops scrolling, and focus returns to the button that opened it. On a later project we used the native dialog element with showModal, which handles the top layer and Escape for you.”

Red flag to avoid:

Raising the z-index to a huge number, which can't escape a stacking context, and ignoring keyboard users.

They may ask next:
  • Do click events inside a portal bubble to its React parent or its DOM parent?
  • How would you test that focus returns to the trigger button?
Say it in 60 seconds
Hard Coding round Mid-level Practice question

17. Your Accordion component kept its own open state, but a new screen needs to open a panel from outside, for example from a link. Change it so it works both ways.

What the interviewer is really testing:
Whether you know the value, default value and change callback pattern that lets a component be controlled or uncontrolled without breaking existing callers.
Answer frame:

Props: openId for controlled use, defaultOpenId for uncontrolled use, and an onOpenChange callback that is always called.

Decide: if openId is passed, the parent owns the state; otherwise the component keeps its own.

Keep callers safe: screens that pass nothing keep working, and one component shouldn't switch modes during its life.

Sample spoken answer:

“I followed the same pattern as a native input with value and defaultValue. The Accordion takes an optional openId, a defaultOpenId and an onOpenChange callback. Inside, it keeps its own state seeded from defaultOpenId, and works out whether it's controlled by checking if openId was passed at all. When it's controlled, it shows the parent's value and only calls onOpenChange, and the parent decides whether to update. When it's uncontrolled, it updates its own state and still calls onOpenChange so a parent can listen. The old screens passed nothing, so they kept working unchanged, and the new screen keeps openId in its own state and sets it when the link is clicked. I also avoid switching between the two modes during one component's life, the same thing React warns about for inputs.”

Code:
function Accordion({ openId, defaultOpenId = null, onOpenChange, items }) {
  const [innerId, setInnerId] = useState(defaultOpenId);
  const isControlled = openId !== undefined;
  const currentId = isControlled ? openId : innerId;

  function toggle(id) {
    const next = currentId === id ? null : id;
    if (!isControlled) setInnerId(next);
    onOpenChange?.(next);
  }

  return items.map((item) => (
    <section key={item.id}>
      <button aria-expanded={currentId === item.id} onClick={() => toggle(item.id)}>
        {item.title}
      </button>
      {currentId === item.id && <div>{item.content}</div>}
    </section>
  ));
}
Red flag to avoid:

Copying the openId prop into state with an effect, so the parent and the component fight over which value is right.

They may ask next:
  • How would you let several panels be open at once without breaking this API?
  • Why is switching from uncontrolled to controlled halfway through a problem?
Say it in 60 seconds
Hard Technical round Mid-level Practice question

18. A phone number input formatted the value as the user typed, adding spaces and a dash. Editing a digit in the middle made the cursor jump to the end. Why, and how did you fix it?

What the interviewer is really testing:
Whether you understand how controlled inputs write values to the DOM, and can handle caret position deliberately.
Answer frame:

Cause: the formatted value differs from what the user just typed, so React writes a new value to the input and the browser moves the caret to the end.

Fix: work out where the caret should land, then restore it with setSelectionRange after the update.

Or simpler: format on blur, or use a well-tested input mask instead of hand-rolled formatting.

Sample spoken answer:

“In a controlled input, when the state value you render is different from what's currently in the DOM, React writes the new value into the input, and the browser puts the caret at the end when a script replaces the value. Our onChange stripped non-digits and re-added the spaces and dash, so the formatted string almost never matched what was typed, and editing in the middle threw the cursor to the end. I fixed it by counting how many digits sat before the caret before formatting, then after the update finding the position after that many digits in the new string and calling setSelectionRange through a ref in a layout effect. For another field we just formatted on blur, which was far simpler and nobody minded. I'd also test paste and delete, because those break naive formatting.”

Red flag to avoid:

Switching the input to uncontrolled and formatting nothing, or blaming the browser without knowing why the caret moves.

They may ask next:
  • Why does deleting a separator character cause extra trouble here?
  • Why can updating a controlled input's state after an await cause the same jump?
Say it in 60 seconds

Real Work 4 questions

Medium Behavioral round Mid-level Practice question

19. Walk me through the last React screen you built from a design file. How did you split it into components and decide where each piece of state lives?

What the interviewer is really testing:
Whether you plan components and state on purpose before coding, and can explain decisions you made on a feature you owned.
Answer frame:

Split: break the design into boxes by responsibility and reuse, not just by how it looks.

State: find the lowest common owner for each piece of state, and separate server data from UI state.

Check: mention what you'd change now and how you confirmed it with design and the backend.

Sample spoken answer:

“The last one was a subscription settings screen with a plan summary, a payment method card, an invoices table and a cancel flow. I sketched boxes over the design first: a page component that loads data, then PlanSummary, PaymentMethod, InvoiceTable and a CancelDialog, and I reused our existing card and table components instead of new ones. For state, server data like the plan and invoices went through our data-fetching hooks, not useState. The invoice table's sort and page only mattered to the table, so they stayed there. The cancel dialog's open state lived in the page, because both the summary and a banner could open it. I checked the API shape with the backend before building, which saved a rewrite. Looking back, I'd put the table's page in the URL.”

Red flag to avoid:

Describing only the look of the screen, with no reasons for where state lives or how the pieces split.

They may ask next:
  • What made you decide to reuse a component instead of building a new one?
  • Where did state end up higher than it needed to be?
Say it in 60 seconds
Medium Behavioral round Mid-level Practice question

20. Which piece of feedback on your React code in a review changed how you build components? What did you do differently afterwards?

What the interviewer is really testing:
Whether you take review well and can name a specific habit that improved, not a general line about learning.
Answer frame:

The comment: quote the actual feedback and the code it was about.

Your reaction: say honestly whether you agreed at first, and what convinced you.

The change: name the habit you kept and a later place it helped.

Sample spoken answer:

“Early on I built a product card that took eleven props, including flags like showBadge, compact and hidePrice, and a senior reviewer wrote that every new screen was going to add another flag. I was a bit defensive at first because it worked, but he showed me the if statements piling up inside it. He suggested composition: a simple Card with slots, passing the badge or the price in as children from each screen. I rewrote it that way, and the next two screens needed no changes to the card at all. Since then, when I catch myself adding a boolean prop that only one caller uses, I stop and ask whether the caller should pass the piece in instead. I pass the same comment on in my own reviews now.”

Red flag to avoid:

Saying you've never had useful feedback, or giving a vague answer like learning to write cleaner code.

They may ask next:
  • Tell me about a review comment you disagreed with. How did it end?
  • How do you review a teammate's React code?
Say it in 60 seconds
Medium Behavioral round Mid-level Practice question

21. Tell me about a React feature that took much longer than you estimated. What did you miss, and how do you estimate now?

What the interviewer is really testing:
Whether you can own a miss, name the real causes, and show a changed way of estimating rather than a promise to try harder.
Answer frame:

The miss: the feature, your estimate and how long it really took.

The causes: the specific things you left out, like states, edge cases or waiting on others.

The change: how you break work down and flag risk now, with a later example.

Sample spoken answer:

“I estimated three days for a file upload widget with drag and drop and a progress bar. It took over two weeks. I'd pictured the happy path, and missed large files needing chunked upload, retry after a dropped connection, cancel, duplicate names, the error and empty states, keyboard access for the drop zone, and tests for all of it. I also waited on a backend change I hadn't asked for early enough. I told my lead as soon as the gaps were clear, not at the deadline, and we cut retry to a later release. Now I list every state a screen can be in, loading, empty, error, partial and success, before I give a number, and I ask about backend needs on day one. My estimates are closer since.”

Red flag to avoid:

Blaming the design or the backend team entirely, with nothing you'd do differently.

They may ask next:
  • How do you tell your lead that a task is going to slip?
  • What do you do when a product manager pushes back on your estimate?
Say it in 60 seconds
Medium Situational round Mid-level Practice question

22. You're asked to add a field to a form and find the same form component copied into three places with small differences. The deadline is this week. What do you do?

What the interviewer is really testing:
Whether you balance delivery with cleanup, and communicate the trade-off instead of silently choosing.
Answer frame:

Size it: check how different the copies really are and how risky merging them would be.

Decide: ship the change safely, merging only if it fits the deadline with tests, otherwise update all three.

Follow up: write a ticket for the merge and tell your lead about the duplication.

Sample spoken answer:

“First I'd diff the three copies to see how different they really are, and check which screens use each one. If the differences are small and there are tests around the form, merging them into one component with a couple of props might fit this week and would make the field a one-place change. If they've drifted a lot or have no tests, merging under a deadline is how things break, so I'd add the field to all three, carefully, and test each screen. Either way I'd tell my lead what I found and write a ticket for merging them, with the list of differences, so it's planned rather than forgotten. Next time someone touches that form, the cleanup is ready to pick up.”

Red flag to avoid:

Refactoring all three silently a day before the deadline, or adding the field to one copy and missing the other two.

They may ask next:
  • What would make you merge them even though it risks the deadline?
  • How would you merge three copies without breaking the screens that use them?
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