All jobs / Frontend / web developer
React interview questions
React questions a Frontend / web developer can be asked, from first job to senior. Open one to see the strongest answer and why it works.
What is JSX, and what does the browser actually receive?
The strongest answer: A syntax for describing UI that compiles to function calls creating element objects; the browser never sees JSX — a build step turns it into plain JavaScript
Why it works: Right. Sugar over createElement; the compiler does the translation.
- JSX compiles to element-creating calls.
- Expressions in braces are JavaScript.
- className, not class.
- A build step is required.
What is the difference between props and state?
The strongest answer: Props are inputs passed from a parent and read-only in the child; state is data a component owns and changes over time, and changing it re-renders the component
Why it works: Right. Owned and mutable versus given and read-only.
- Props: passed in, read-only.
- State: owned, changed with a setter.
- Setting state re-renders.
- Lift state up when siblings share it.
What does useState return, and why must you call the setter instead of assigning?
The strongest answer: A pair: the current value and a setter; React re-renders only when the setter is called, so assigning to the variable changes nothing on screen
Why it works: Right. The setter is how React learns something changed.
- const [value, setValue] = useState(initial).
- Assigning the variable does nothing.
- Setter schedules a re-render.
- Functional updates when the next depends on the previous.
Why does React warn about a missing key when you render a list?
The strongest answer: Keys let React match each rendered item to its previous version across renders, so reordering, inserting or deleting does not scramble state and DOM; a stable, unique id per item is the right key, not the array index
Why it works: Right. Identity across renders; the index breaks when the list changes order.
- Key = stable identity per item.
- Use ids, not the index.
- Index keys break on reorder or delete.
- Keys must be unique among siblings.
What is a controlled input?
The strongest answer: An input whose value comes from state and whose onChange updates that state, so React is the single source of truth for what the field shows
Why it works: Right. Value from state, change into state.
- value={state} onChange={set}.
- React owns the field's value.
- Uncontrolled: the DOM owns it, read via ref.
- Do not switch between the two.
What is useEffect for?
The strongest answer: Running side effects after render — fetching data, subscribing, syncing with something outside React — with a cleanup function and a dependency list that says when to run again
Why it works: Right. After render, for things React itself does not do.
- Effects run after render.
- Return a cleanup.
- Dependencies decide when it re-runs.
- Not for deriving state.
What happens if useEffect has an empty dependency array, and if it has none at all?
The strongest answer: Empty array: runs once after the first render; no array: runs after every render; the array lists the values the effect reads so it re-runs when they change
Why it works: Right. The array is the list of things the effect depends on.
- []: after the first render only.
- No array: after every render.
- [a, b]: when a or b changes.
- Lint keeps the list honest.
How do you render something only when a condition holds?
The strongest answer: A JavaScript expression in JSX: cond && <X />, a ternary for two branches, or an early return — JSX is expressions, so ordinary logic works
Why it works: Right. Expressions in braces; if statements go above the return.
- && for one branch, ternary for two.
- Early return for the whole component.
- Watch 0 && rendering a zero.
- CSS hiding is not conditional rendering.
Why does onClick={handleClick()} fire during render instead of on click?
The strongest answer: Because the parentheses call the function immediately while rendering; pass the function itself — onClick={handleClick} — or an arrow that calls it
Why it works: Right. Pass a function, do not call it.
- Pass the function, not its result.
- Arrow when you need arguments.
- Events are camelCase.
- The event object is React's synthetic event.
Two sibling components need the same piece of data. Where does it live?
The strongest answer: In their closest common parent, passed down as props with a setter passed to whoever changes it — lifting state up
Why it works: Right. One owner above both.
- Lift to the common parent.
- Pass value down, setter down.
- One source of truth.
- Context when the prop chain gets long.
Why does a component return a fragment instead of a wrapping div?
The strongest answer: A component must return one root; a fragment groups children without adding a DOM node, so the layout and CSS are not disturbed by a wrapper that exists only for React
Why it works: Right. Group without a wrapper element.
- One root per return.
- Fragment adds no DOM node.
- <>...</> shorthand.
- Keyed fragments in lists.
Why does items.push(x); setItems(items) not re-render?
The strongest answer: Because the array is the same object and React compares by reference; create a new array — setItems([...items, x]) — so the reference changes
Why it works: Right. New object, new reference, re-render.
- State is compared by reference.
- Never mutate state in place.
- Spread into a new array or object.
- Same for nested objects.
When is a React piece a reusable component versus a page-level screen?
The strongest answer: Components own a UI chunk and props; pages own the route, data loading, and composition
Why it works: This is architecture at junior scale: reuse the card, keep the route's data at the page.
- Split route from reusable UI.
- Page: URL, load data, compose.
- Component: props in, UI out.
- Don't hide fetching inside a presentational button.
In-app navigation full-reloads the page. What did the markup do wrong?
The strongest answer: Use the router's Link for internal routes — a plain <a href> without the router does a full load
Why it works: Client routing is a Link (or useNavigate) that prevents the browser load. Anchors still matter for new tabs and progressive enhancement when the router handles them.
- Internal moves use the router.
- Link (or the framework equivalent) for in-app paths.
- Real <a> for external URLs.
- Don't assign location to move inside the SPA.
A useEffect fetches on mount and when userId changes, but the UI sometimes shows the previous user's data. Strongest approach?
The strongest answer: Put userId in the dependency array and abort or ignore stale responses in cleanup
Why it works: Deps plus cancellation (AbortController or an ignore flag) is the complete answer.
- Effects must match their data.
- List every value the effect reads.
- Cancel or ignore the in-flight request on change/unmount.
- Don't disable the exhaustive-deps rule to hide it.
A sortable list uses key={index} and inputs lose values when you reorder. Why, and what instead?
The strongest answer: Use a stable id as the key so React reconciles the same child; index keys break on insert and reorder
Why it works: Keys are identity. Index identity moves with position, so state follows the wrong row.
- Keys are identity, not position.
- Give each row a stable id.
- Don't use index for sortable or insertable lists.
- Never randomize keys.
You subscribe to a websocket in useEffect. What must the cleanup do?
The strongest answer: Unsubscribe or close on unmount and on dependency change so you don't leak sockets or setState after unmount
Why it works: Cleanup is the other half of the effect. Dep changes re-run it — close the previous socket.
- Every subscribe needs an unsubscribe.
- Return cleanup from the effect.
- Close on unmount and when deps change.
- Don't setState after the socket is gone.
A search field and the URL get out of sync. How do you keep the input controlled?
The strongest answer: Pick one source of truth — state or the URL — set value and onChange, and derive the other
Why it works: Controlled means the value you render is the value you store. URL-as-source is a good pattern if you commit to it.
- One source of truth for the field.
- Either React state or the router search params.
- value + onChange (controlled).
- Derive the other representation; don't dual-write blindly.
A setInterval inside useEffect logs a stale counter. How do you fix it?
The strongest answer: Include the changing value in the effect deps and reset the interval, or read the latest value from a ref you update
Why it works: The interval closure is created once unless deps change — refs or reset are the two honest fixes.
- Closures capture render values.
- Either restart the interval when the value changes,
- or keep a ref with the latest value.
- Don't disable deps to 'keep one interval' without a ref.
The user navigates away mid-fetch and you see a state-update warning or a flicker. What do you do?
The strongest answer: AbortController in the effect cleanup (or ignore stale), and do not setState after cancel
Why it works: Cancelled work is part of routing. Abort and don't commit state for a screen that is gone.
- Cancel work you no longer need.
- Abort in cleanup.
- Ignore the response if it still arrives.
- Never block navigation on a list fetch.
A widget throws during render and blanks the whole app. What should have been there?
The strongest answer: Error boundaries around independent UI, with a fallback and a log — and separate handling for events and async, which boundaries don't catch
Why it works: Boundaries isolate render errors. Root-only still blanks the shell; missing ones blank everything.
- Isolate render failures.
- Wrap independent widgets in error boundaries.
- Show a fallback; log the error.
- Handle async and event errors outside the boundary.
Theme and a one-off modal title — Context or props?
The strongest answer: Props for local data; Context (or a store) for wide, low-frequency values like theme or auth — don't context every keystroke
Why it works: Context is for values many distant consumers share. A title two levels down is a prop (or composition).
- Match the channel to the distance.
- Props or children for local UI.
- Context for theme, locale, auth.
- Don't put high-frequency input state in Context.
Two screens share the same fetch, loading, and error state. How do you share it?
The strongest answer: Extract a custom hook (or a query library) that returns data and status; keep markup out of the hook
Why it works: Hooks share behaviour. Query libraries are the 2026 default when cache and races matter.
- Share behaviour, not markup.
- useThing() returns { data, status, error }.
- Or use Query/SWR for cache.
- Don't write to the DOM from a singleton.
A teammate wraps every component in React.memo. How do you respond?
The strongest answer: Memoize when profiling shows wasted renders and props stay stable — memo has a comparison cost and can hide stale bugs
Why it works: memo is a tool you apply after a profile, not a house style.
- Measure, then memoize.
- Profile the wasted renders.
- Stabilize props or hoist them.
- Don't wrap the tree by default.
The React Compiler is on. Where do you still think about memoization?
The strongest answer: Keep referential stability for non-React consumers — effect deps, observers, imperative APIs — and don't sprinkle useMemo by habit; still measure
Why it works: The compiler removes a lot of manual memo, not the need to understand identity and algorithm cost.
- The compiler is not a substitute for complexity.
- Stop habit-memoizing UI.
- Still stabilize values other systems observe.
- Profile hot paths; fix algorithms first.
A list refetches on every tab focus and feels jumpy. What do you tune?
The strongest answer: Set staleTime and gcTime to how fresh the data must be, invalidate on mutations, and don't refetch everything by default
Why it works: TanStack Query / SWR are freshness policies. refetchOnWindowFocus is a default, not a law.
- Cache is a product decision.
- Choose staleTime for the resource.
- Invalidate the keys you mutate.
- Don't fight the cache with per-component fetch.
After POST /comments the list still shows the old data. What did the cache miss?
The strongest answer: Invalidate or update the exact query keys that own that list (and the nested detail)
Why it works: Keys are the cache API. Mutation success should touch the list key or write the new row in.
- Mutations must update the keys they change.
- Name the list key.
- invalidateQueries or setQueryData.
- Don't keep a shadow copy in useState.
A like button uses optimistic UI. The request fails. What must happen?
The strongest answer: Roll back local state and the cache, show the error, and leave the action retryable
Why it works: Optimistic UI is a loan: failure must repay it. Users should see the truth and be able to try again.
- Optimism needs a rollback.
- Apply the local like.
- On error, revert cache and UI.
- Tell the user; allow retry.
Optimistic create plus a refetch returns the old list without the item. What do you guard?
The strongest answer: Reconcile with the server using stable ids — don't assume refetch order — and keep the mutation response as the source when it arrives
Why it works: Caches lie when writes and reads race. Ids and mutation payloads are how you merge.
- Races need ids, not hope.
- Optimistic insert with a temp or real id.
- Merge on mutation response.
- Treat refetch as truth, not as 'older'.
A lazy route flashes a blank screen, then an error. How do you structure Suspense and error boundaries?
The strongest answer: Put an error boundary around (or above) Suspense, use route-level fallbacks, and don't let one widget blank the shell
Why it works: Suspense handles wait; error boundaries handle throw. Nested fallbacks keep the chrome up.
- Pair wait and failure.
- Error boundary + Suspense at the route.
- Local fallbacks for widgets.
- Keep the app shell mounted.