Ch. 03

React interview questions & answers

How React really renders: hooks, reconciliation, keys, memoization, state management and effects.

127 interview questions26 quiz questions24 notes
your progress0%

Notes in this chapter

Filter all notes →
read ✓React · hard

React Context Splitting for Performance

Why every React context consumer re-renders when any part of the value changes, and how splitting state, actions and slow-changing data fixes it.

~6 min readread →
read ✓React · mid

React lazy Loading and Code Splitting

Split React bundles with lazy and Suspense, preload chunks before the click, and handle the chunk-load failures that follow every deploy.

~7 min readread →
read ✓React · mid

React Portals and Event Bubbling

Events from a React portal bubble through the React tree, not the DOM. Here is what that breaks in click-outside logic, and how to fix it.

~6 min readread →
read ✓React · mid

React StrictMode and Double-Invoked Effects

What StrictMode calls twice in development, the exact mount, unmount, mount sequence in React 19, and how to fix the effects it exposes.

~6 min readread →
read ✓React · hard

React useOptimistic and Instant Feedback

Show a sent message before the server answers with useOptimistic, see how it rolls back on failure, and avoid the outside-transition trap.

~6 min readread →
read ✓React · mid

React Deferred Results and Stale UI

Keep a search input responsive with useDeferredValue, show users when results are stale, and see why memo is required and fetches aren't reduced.

~6 min readread →
read ✓React · hard

React Dialogs: Focus, Escape and Restoration

Build an accessible React modal: native dialog with showModal, focus on open, Escape to close, focus restoration and testing with keyboard only.

~7 min readread →
read ✓React · easy

React Derived Values Without Extra State

Stop syncing state with useEffect: compute filtered lists, totals and validity during render in React, and use useMemo only for real cost.

~6 min readread →
read ✓React · hard

React Error Boundaries and Recovery Scope

What React error boundaries catch and miss, where to place them so one failing widget does not blank the page, and how to reset after errors.

~7 min readread →
read ✓React · hard

React Hydration and Matching Initial Content

What causes React hydration mismatch errors: dates, random ids, window checks and bad HTML nesting, plus fixes with useId, effects and Suspense.

~7 min readread →
read ✓React · easy

React List Keys: Why Index Keys Break State

Why React list keys matter: how index keys attach state to the wrong row after deletes or reordering, and how to pick a stable, unique key.

~6 min readread →
read ✓React · mid

React Reducers and Valid State Transitions

When to choose useReducer over useState: model status as one field, reject impossible transitions in the reducer, and test it in plain Node.

~6 min readread →
read ✓React · easy

React Refs Versus State for Mutable Values

useRef vs useState in React: refs change without re-rendering, state drives the UI. Learn which values belong where, with timer and DOM examples.

~6 min readread →
read ✓React · easy

React Resetting Forms With Component Keys

Reset a React form when the selected record changes by giving it a key: how keys control state identity and why useEffect resets go wrong.

~6 min readread →

127 React interview questions study by subtopic

127 questions
  1. 1.How would you design a reusable card component without ending up with dozens of display flags?mid

    Flags pile up when the card tries to know every layout: showHeader, headerIcon, showFooter, footerAlign, compact. Each new screen adds one, and soon nobody can test the combinations.

    I'd make the card handle only the frame, meaning padding, border, radius and elevation, and let callers pass content. children covers the body and named slots cover the rest: <Card header={<h3>Pro plan</h3>} footer={<Button>Upgrade</Button>}>. For more structure, compound components work well: Card.Header, Card.Body and Card.Footer share styling through context, and callers arrange them.

    Variants are for design rules that genuinely repeat, like variant: 'outlined' | 'elevated', typed as a union so a typo fails to compile. Domain logic, such as whether a plan is the user's current one, stays in the caller.

    Semantics matter too: the caller picks the heading level, since the card can't know where it sits on the page, and a clickable card shouldn't nest a button inside a link. I'd test the few combinations the product actually uses.

    What interviewers listen for
    • The card handles the frame; content comes through children
    • Named slot props or compound components
    • Variants only for repeated design rules, typed as a union
    • Domain logic stays in the caller
    • The caller picks the heading level

    Likely follow-up: When would you choose compound components with context over slot props?

  2. 2.Why does declaring a component inside its parent's render reset a form, as in this snippet?mid

    Because EmailField is a new function every time SignupForm renders. React keeps a component's state only while the element at the same position has the same type, and for a component the type is the function itself. When the user picks a plan, SignupForm re-renders and creates a different EmailField, so React sees a new component type. It unmounts the old one and mounts a fresh one with email back to an empty string, and the input loses focus.

    The fix is defining EmailField at module level and passing what it needs as props. If it needs parent state, pass the value and a setter, or lift the state up.

    The same reset happens with a key that changes every render, like key={Math.random()}, or with lazy() called inside a component. When you want a reset, use a deliberate key, such as key={userId}.

    A quick test: type an email, change the plan, and assert the email is still there.

    function SignupForm() {
      const [plan, setPlan] = useState('free');
    
      function EmailField() {
        const [email, setEmail] = useState('');
        return <input value={email} onChange={(e) => setEmail(e.target.value)} />;
      }
    
      return (
        <>
          <EmailField />
          <PlanPicker value={plan} onChange={setPlan} />
        </>
      );
    }
    What interviewers listen for
    • A nested function is a new component type on every render
    • Same type in the same position keeps its state
    • Define components at module level and pass props
    • Unstable key values cause the same reset
    • Use a deliberate key when you want a reset

    Likely follow-up: How would you reset a form on purpose when the user switches to a different record?

  3. 3.How would you store the selected item in a list that refreshes frequently?mid

    Store the id, not the object. const [selectedId, setSelectedId] = useState<string | null>(null), then derive the record during render with items.find(i => i.id === selectedId) ?? null. For a big list, a Map built with useMemo makes that lookup cheap.

    If you store the whole object instead, the selection is a copy taken at click time. The list refreshes, the price or status changes, and the details panel keeps showing old values because nothing updates the copy. You now have two sources of truth that drift apart.

    Deriving also forces you to handle the case people forget: the selected record disappears after a refresh, because someone deleted it or a filter now excludes it. The derived value becomes null, and you decide what that means, either clearing the selection or showing 'This item is no longer available' so the user isn't confused. I'd write a test for exactly that.

    The same rule applies to anything you can compute: keep the minimal source, here the id, and calculate the rest during render.

    What interviewers listen for
    • Store the selected id, not a copy of the object
    • Derive the record from the current list
    • A copied object goes stale after refreshes
    • Handle the selected item disappearing

    Likely follow-up: Where would the selected id live if the selection had to survive a reload or be shared as a link?

  4. 4.A settings form receives refreshed server data while the user is editing. How should it behave?mid

    Keep the saved record and the user's draft separate, with the draft starting as a copy. What you shouldn't do is useEffect(() => setDraft(record), [record]): a background refetch, such as TanStack Query refetching on window focus, then wipes whatever the user was typing.

    Two events look similar but need different handling. Switching to a different record should reset the draft, and the clean way is <SettingsForm key={record.id} record={record} />, which remounts the form. A refresh of the same record shouldn't touch the user's edits.

    For refreshes, compare the draft with the record to find dirty fields. If nothing is dirty, adopt the new data. If the user has edits and the server version changed, show a banner such as 'This was updated elsewhere' with options to reload or keep editing.

    On save, send the version or ETag the draft started from. If the server answers 409 because someone else saved first, you can show the conflict instead of silently overwriting their change.

    What interviewers listen for
    • Separate the saved record from the draft
    • Don't copy props into state with an effect
    • key={record.id} resets on a record switch
    • Adopt refreshes only when nothing is dirty
    • Send a version and handle 409 conflicts

    Likely follow-up: How would you track which individual fields are dirty?

  5. 5.How would you avoid breaking text input for users typing with an IME, such as a Japanese, Chinese or Korean input method?hard

    An IME builds characters in stages. While composing, the input holds provisional text, and React's onChange fires at each step because it follows the native input event. Code that treats every change as final text breaks that.

    The common breakages:

    • Formatting during composition. A controlled input that uppercases, masks or trims value on each change rewrites the text mid-composition, which can cancel the composition or duplicate characters. Format on blur, or after onCompositionEnd.
    • Searching on every change. Skip events while composing, using e.nativeEvent.isComposing, and search when composition ends.
    • Enter to submit. Pressing Enter to confirm a candidate also fires keydown. Ignore it while isComposing is true; in Safari that keydown can arrive with isComposing false, so also check keyCode === 229.

    Android keyboards like Gboard use composition even for English words, so this isn't only about Asian languages.

    I'd test with real IMEs: Japanese on macOS and Windows, and Gboard on Android. Simulated key events in unit tests won't reproduce these bugs.

    What interviewers listen for
    • onChange fires during composition
    • Don't reformat the value mid-composition
    • Check isComposing before searching or submitting on Enter
    • In Safari, also check keyCode 229
    • Test with real IMEs, including Android keyboards

    Likely follow-up: How would you build search-as-you-type so it waits for composition to finish?

  6. 6.How would you prevent accidental double submission of a React form?mid

    On the client, show a pending state and block repeats. With React 19 form actions, useActionState returns [state, formAction, isPending], and a submit button rendered inside the form can read useFormStatus().pending. Disabling the button while pending also stops Enter from resubmitting, because implicit submission does nothing when the default button is disabled. With a plain onSubmit, a ref flag set at the start catches a second event that arrives before the re-render.

    That reduces duplicates but can't rule them out: a request can time out after the server committed it, and the user retries. So the server enforces it. The form sends an idempotency key generated once with crypto.randomUUID(), and the server returns the original result for a repeated key. A unique constraint on the order reference adds a second guard.

    After a failure, the button re-enables and the input stays. In React 19, uncontrolled fields reset after a form action finishes, even when it returned an error, so return the submitted values and feed them back through defaultValue.

    What interviewers listen for
    • isPending or useFormStatus().pending disables submit
    • A ref guard for plain onSubmit handlers
    • Server-side idempotency key and unique constraints
    • Keep the input after a failure
    • React 19 resets uncontrolled fields; use defaultValue

    Likely follow-up: Why must useFormStatus be called from a component inside the form, not from the component that renders the form?

  7. 7.How would you manage a chat connection in React when the user switches rooms?hard

    The connection depends on the room, so one effect creates it with roomId as its dependency, and its cleanup closes it. When the room changes, React runs the old room's cleanup before the new setup, so you never hold two connections. Strict Mode in development connects, disconnects and reconnects on mount, which tests the cleanup.

    Listeners belong in the same effect, so they go away with the connection. The awkward part is values the handler reads that shouldn't cause a reconnect, like muted here. Listing muted as a dependency would drop and reopen the socket every time the user toggles sound. useEffectEvent, stable since React 19.2, gives a handler that always sees the latest props without being a dependency. On older versions, a ref holding the latest value does the same job.

    To clear the previous room's messages, the parent renders the component with key={roomId}, so each room gets fresh state. I'd test fast room switching and assert there's one open connection and no duplicate messages.

    function ChatRoom({ roomId, muted }) {
      const [messages, setMessages] = useState([]);
    
      const onMessage = useEffectEvent((msg) => {
        setMessages((list) => [...list, msg]);
        if (!muted) playSound();
      });
    
      useEffect(() => {
        const conn = createConnection(roomId);
        conn.on('message', (msg) => onMessage(msg));
        conn.connect();
        return () => conn.disconnect();
      }, [roomId]);
    
      return <MessageList messages={messages} />;
    }
    
    // Parent: <ChatRoom key={roomId} roomId={roomId} muted={muted} />
    What interviewers listen for
    • One effect per connection, with roomId as the dependency
    • Cleanup disconnects before the next room connects
    • Listeners live in the same effect
    • useEffectEvent for values that shouldn't reconnect
    • Test fast switching: one connection, no duplicates

    Likely follow-up: What goes wrong if onMessage is a regular function listed in the effect's dependencies?

  8. 8.With Strict Mode on, this effect leaves two live subscriptions in development. What's wrong, and how would you fix it?mid

    Strict Mode runs setup, cleanup, then setup again on mount in development. It simulates a remount, which real apps also do with Fast Refresh or React's <Activity>. If two subscriptions survive, the cleanup doesn't fully undo the setup.

    Here the setup is asynchronous. The first cleanup runs before subscribe() resolves, while sub is still undefined, so it does nothing, and that first subscription lives on beside the second. The fix is a flag: let cancelled = false in the effect, set to true in cleanup, and when the promise resolves, unsubscribe at once if cancelled is true.

    Other causes: removing a listener with a different function than the one added, such as a fresh arrow function in removeEventListener, or unsubscribing from a different object.

    I wouldn't add a useRef(false) 'already subscribed' guard: it silences the check but breaks on real remounts. And I'd verify by counting live subscriptions, not console logs.

    useEffect(() => {
      let sub;
      client.subscribe(channel).then((s) => {
        sub = s;
      });
      return () => sub?.unsubscribe();
    }, [channel]);
    What interviewers listen for
    • Strict Mode runs setup, cleanup, setup on mount
    • Async setup needs a cancelled flag
    • Remove listeners with the same function reference
    • Don't hide it with a ref guard
    • Count live subscriptions, not logs

    Likely follow-up: Why does React run this check, and which real features depend on effects surviving a remount?

  9. 9.What makes a custom Hook's API easy to use correctly?mid

    It returns what callers need in domain terms. A useCart() that returns { items, total, addItem, removeItem } is harder to misuse than one that hands out raw setItems and lets every caller invent its own update logic. For two values a tuple like useState's works; beyond that, named fields avoid order mistakes.

    Functions it returns should keep their identity between renders, through useCallback or the React Compiler, because callers will put them in effect dependency arrays. An onChange that changes every render makes their effects re-run constantly.

    State must live in React, not in module scope. A let cache = [] at the top of the hook's file is shared by every component using the hook. That's a bug unless sharing is the point, and then a real store read through useSyncExternalStore fits better.

    Document what isn't obvious: what happens on unmount, whether it's safe during SSR, and what errors look like, such as { status: 'error', error }. I'd test it with renderHook, and with two components using it at once.

    What interviewers listen for
    • Return domain operations, not raw setters
    • Stable function identity for dependency arrays
    • No module-level state unless sharing is deliberate
    • Document unmount, SSR and error behavior
    • Test with renderHook and two consumers

    Likely follow-up: When should a custom Hook take an options object instead of positional arguments?

  10. 10.When would you replace several useState calls with useReducer?mid

    When pieces of state change together and some combinations should be impossible. A checkout wizard with step, values, isSubmitting, error and isDone in separate useState calls can end up with isSubmitting and error both set, or isDone while still on step 2, because every handler must update four things correctly.

    With a reducer, each transition is one named action: next, back, submitStarted, submitSucceeded, submitFailed, reset. The rules live in one function: submitFailed sets the error and returns to editing, and back is ignored while submitting. Modeling status as 'editing' | 'submitting' | 'done' instead of booleans removes most impossible states outright.

    Reducers must be pure, so the network call stays in the event handler: dispatch submitStarted, await the request, then dispatch success or failure. Strict Mode calls reducers twice in development, which exposes hidden side effects.

    Testing gets easier too, since the reducer is a plain function and reducer(state, action) needs no rendering. For two independent fields, though, useState stays simpler.

    What interviewers listen for
    • A reducer when state changes together
    • Named actions keep the rules in one place
    • A status union instead of several booleans
    • Pure reducers; network calls in handlers
    • Test the reducer as a plain function

    Likely follow-up: How would you type the actions so TypeScript rejects one with the wrong payload?

  11. 11.How do you decide whether a value belongs in React context?mid

    I ask three questions: who reads it, how often it changes, and who updates it.

    Context fits values that many components at different depths need and that change rarely: theme, locale, the signed-in user, feature flags. In React 19 you render <ThemeContext value={theme}> and read it with useContext or use.

    The cost is how updates spread. Every component that reads a context re-renders when its value changes by Object.is, and there's no way to subscribe to just one field. Passing value={{ user, setUser }} creates a new object on every render, so every consumer re-renders even when nothing changed. Memoize the value, or split state and setter into two contexts so components that only update don't re-render.

    Context also isn't a data cache. It has no deduplication, no staleness and no refetching, so server data belongs in a query library. State used by one component or a small subtree should stay local or travel as props, and passing JSX as children often removes the prop drilling that made context tempting.

    What interviewers listen for
    • Who reads it, how often it changes, who updates it
    • Good for theme, locale, user and feature flags
    • Every consumer re-renders on a new value
    • Memoize or split the value
    • Not a cache for server data

    Likely follow-up: How would you stop components that only call the setter from re-rendering?

  12. 12.Why separate server data from local UI state in a React app?mid

    Server data is a copy of something that lives elsewhere: it goes stale, needs refetching, can fail and retry, should be deduplicated when several components ask for it, and must be invalidated after a mutation. UI state, like an open panel, the selected tab or an unsaved draft, lives only in the browser and has none of those concerns.

    A query library like TanStack Query handles the server side. useQuery({ queryKey: ['orders', { status }], queryFn }) caches by key, dedupes concurrent requests and refetches stale data, and invalidateQueries({ queryKey: ['orders'] }) after a mutation keeps lists fresh. UI state stays in useState, a reducer or a small client store.

    The anti-pattern is fetching in an effect and copying the response into Redux or several useState calls. Then you maintain a cache by hand: when to refetch, how to avoid races, and which copy is current.

    Every input that changes the result, like filters, page or user, belongs in the query key, or different queries share one cache entry.

    What interviewers listen for
    • Server data goes stale and needs refetching and invalidation
    • UI state is local: panels, tabs, drafts
    • A query library caches by key and dedupes
    • Don't copy responses into several stores
    • Every result-changing input goes in the key

    Likely follow-up: In an app using Server Components, how much of this still applies on the client?

  13. 13.A React dashboard feels slow. How would you find the bottleneck?mid

    I'd pick one specific slow interaction, like typing in the filter box or switching tabs, and reproduce it with realistic data on a production build, since development builds are much slower.

    Then I'd record it with two tools. The React DevTools Profiler shows which components rendered in each commit, how long they took and, with 'Record why each component rendered' turned on, why. The Chrome Performance panel shows long tasks, layout and network, plus React's own tracks since React 19.2. Together they show which cost dominates: too many components re-rendering, one expensive component, layout thrashing or a slow request.

    Common findings: a context value recreated every render so the whole tree re-renders, thousands of rows rendered at once, sorting or filtering inside render without useMemo, or a chart library being re-created on every update.

    I'd fix the biggest cost first, then record the same interaction again and compare the numbers. Adding memo everywhere before measuring usually adds complexity without touching the slow part.

    What interviewers listen for
    • Reproduce one interaction with real data on a production build
    • React Profiler: what rendered, how long, and why
    • Chrome Performance panel: long tasks and layout
    • Common causes: context churn, big lists, work in render
    • Fix the biggest cost, then measure again

    Likely follow-up: How would you tell whether a component re-rendered because of props, state or context?

  14. 14.What trade-offs come with virtualizing a long React list?hard

    The gain is clear: with a library like @tanstack/react-virtual or react-window, a 10,000-row list keeps maybe 30 rows in the DOM, so mounting, rendering and memory drop sharply.

    The costs show up wherever code assumed every row exists:

    • Sizing. The library needs row heights. Fixed heights are easy; variable heights need measuring, and wrong estimates make the scrollbar jump.
    • Identity. Rows mount and unmount as you scroll, so keys must be stable ids, and row-local state, like an expanded section, is lost unless lifted up.
    • Find in page. Ctrl+F can't find rows that aren't rendered.
    • Focus and screen readers. A focused row that scrolls out unmounts and focus is lost. Screen readers only see rendered rows, so you need aria-rowcount and aria-rowindex, or aria-setsize and aria-posinset.
    • Scroll restoration when the user returns.

    For a few hundred rows I'd rather use pagination, or CSS content-visibility: auto, which skips rendering offscreen content while keeping it in the DOM, so find in page still works.

    What interviewers listen for
    • Far fewer DOM nodes and faster renders
    • Needs row sizes; variable heights need measuring
    • Row state is lost on unmount; use stable keys
    • Find in page, focus and screen readers suffer
    • Small lists: pagination or content-visibility

    Likely follow-up: How would you keep keyboard focus stable while the user arrows through a virtualized list?

  15. 15.How would you keep a search box responsive while rendering expensive results?hard

    The input must update immediately, so its state stays urgent, and the expensive part renders from a deferred copy. With useDeferredValue(query), React first re-renders with the old deferredQuery, so the keystroke appears at once, then renders the new results in the background, abandoning that work if another key arrives.

    Two details make it work. Results must be wrapped in memo; otherwise it re-renders during the urgent pass anyway and nothing is gained. And comparing query !== deferredQuery gives a stale flag, so old results can dim instead of being replaced by a spinner. startTransition around a separate filter state does the same job when you control the setter.

    Network requests need their own policy: debounce them, and ignore or abort out-of-order responses.

    The limit is CPU. React yields to the browser between components, but one component that sorts 50,000 items synchronously still blocks the main thread for that long. Then the fix is less work: useMemo for the sort, virtualization, or a Web Worker.

    function Search() {
      const [query, setQuery] = useState('');
      const deferredQuery = useDeferredValue(query);
      const isStale = query !== deferredQuery;
    
      return (
        <>
          <input value={query} onChange={(e) => setQuery(e.target.value)} />
          <div style={{ opacity: isStale ? 0.6 : 1 }}>
            <Results query={deferredQuery} />
          </div>
        </>
      );
    }
    
    // memo lets Results skip the urgent render while deferredQuery is unchanged
    const Results = memo(function Results({ query }) {
      const rows = useMemo(() => filterProducts(query), [query]);
      return <ProductList rows={rows} />;
    });
    What interviewers listen for
    • Keep the input's state urgent
    • useDeferredValue or startTransition for results
    • Wrap the slow component in memo
    • Dim stale results with query !== deferredQuery
    • Heavy CPU work still blocks: memoize, virtualize or use a worker

    Likely follow-up: When would you choose useTransition over useDeferredValue?

  16. 16.Now that React can restart or discard a render, what's wrong with a component like this one?hard

    It assumes each render happens once and always reaches the screen; neither is true. A transition can be interrupted and restarted, a component can suspend and render again later, and Strict Mode renders twice in development. Rendering should only calculate JSX from props and state.

    All three lines break that rule:

    • renderCount++ mutates a module variable, so the count is wrong in Strict Mode and after interrupted renders.
    • analytics.track during render can fire twice, or fire for a render that was thrown away. It belongs in an effect, or in the event handler that caused the view.
    • use(fetch(...)) creates a new promise on every render. The component suspends, renders again, makes another promise and suspends again. A promise passed to use must come from a cache, a framework loader, or a parent that created it once.

    Two related rules: read external mutable stores through useSyncExternalStore, so different components can't show different versions within one render, and don't read or write ref.current during render, except to initialize it lazily.

    let renderCount = 0;
    
    function ProductRow({ product }) {
      renderCount++;
      analytics.track('product_viewed', { id: product.id });
      const price = use(fetch(`/api/price/${product.id}`).then((r) => r.json()));
      return <li>{product.name}: {price.amount}</li>;
    }
    What interviewers listen for
    • Renders can repeat, restart or be discarded
    • No mutations or analytics during render
    • Promises passed to use must be cached
    • useSyncExternalStore for external stores
    • Don't read or write refs during render

    Likely follow-up: Where would you move the analytics call, and how would you avoid double-counting views?

  17. 17.How can you reduce data waterfalls on a screen built with Suspense?hard

    A waterfall happens when each component starts its own fetch while rendering. ProfilePage fetches the user and suspends; only when that resolves does Posts render and start its fetch, and then Comments. Three requests that could run together take three round trips.

    The fix is starting independent requests before anything renders. In a framework that means the route loader or the Server Component: call getUser(id) and getPosts(id) without awaiting, then await both or pass the promises down. On the client in React 19, a parent can create the promises once, ideally through a cache, and children read them with use(promise). TanStack Query offers prefetchQuery and useSuspenseQueries for the same idea.

    Only truly dependent requests stay sequential, such as needing the user's teamId before fetching the team.

    Suspense boundaries decide what the user sees, not when requests start. Adding more Suspense boundaries lets the header appear while posts load, but doesn't parallelize anything. I'd compare the Network panel's waterfall before and after.

    What interviewers listen for
    • Fetching inside nested components causes waterfalls
    • Start independent requests before rendering
    • Pass promises down and read them with use()
    • Only dependent requests stay sequential
    • Suspense boundaries control reveal, not request timing

    Likely follow-up: How would you prefetch a route's data when the user hovers its link?

  18. 18.How would you handle a lazy-loaded chunk failing to load after a deployment?hard

    This hits users who loaded the app before a deploy. Their old index.html references chunk files with old hashes, the deploy removed those files, and import() rejects with something like 'Failed to fetch dynamically imported module'. lazy throws that to the nearest error boundary; without one, the whole app unmounts and the user sees a blank page.

    Prevention comes first: keep old hashed assets on the CDN for a while after deploying, and upload new assets before switching the HTML.

    For the rest, the error boundary should say a new version is available and offer a reload button. A 'try again' that only resets the error boundary won't work, because lazy caches the rejected promise and throws the same error again.

    Automatic reloading is fine with a guard: reload once, record that in sessionStorage, and show the message if it fails again, or a truly broken chunk causes an endless reload loop. Vite apps can catch this centrally through the vite:preloadError event. Save any draft before reloading.

    What interviewers listen for
    • Old HTML points at deleted chunk files
    • Keep old assets after deploying
    • An error boundary with a reload button
    • lazy caches the rejection, so a reset alone fails
    • Reload once with a guard; save drafts first

    Likely follow-up: How would you tell a stale-chunk error apart from the user simply being offline?

  19. 19.How should a form behave when submission fails because the user is offline?mid

    Never lose what they typed, and never pretend it was sent. When fetch rejects with a TypeError and navigator.onLine is false, I keep the form as it is and say plainly: 'You're offline. Your changes haven't been sent.' The draft is also saved to localStorage or IndexedDB, so a reload or a closed tab doesn't lose it.

    Then offer a way forward: a Retry button, plus an automatic retry when the online event fires, confirmed by a request succeeding, since navigator.onLine only says a network interface exists.

    If the product needs true offline submission, queue the request, but show it as 'Waiting to send', never 'Saved', until the server confirms. Each queued submission carries an idempotency key, so a retry after an unclear timeout doesn't create two orders.

    This failure should look different from others. Offline means try again later, a 422 means fix these fields, and a 500 means something broke on our side. Lumping them into one 'Something went wrong' toast leaves the user guessing.

    What interviewers listen for
    • Keep the draft and persist it locally
    • Say clearly that nothing was sent
    • A Retry button plus retry on the online event
    • Queued means waiting, not saved; use idempotency keys
    • Different messages for offline, 422 and 500

    Likely follow-up: How would you test this flow without actually disconnecting the network?

  20. 20.What would you test for a React error boundary?mid

    First, what it catches. Render it around a test component that throws during render and assert the fallback appears, such as screen.getByRole('alert') with a useful message. Error boundaries catch errors thrown while rendering, in lifecycle methods and in constructors below them, but not in event handlers, async code or the error boundary itself. React logs caught errors to the console, so I stub console.error in these tests to keep output readable.

    Second, isolation. Siblings outside the error boundary still render and stay usable, which is the whole point of wrapping one widget rather than the page.

    Third, reporting. The onError callback or componentDidCatch sends the error and componentStack to monitoring, and the test checks that no tokens or personal data go with it.

    Fourth, recovery. With react-error-boundary, clicking 'Try again' calls resetErrorBoundary, and a change in resetKeys, such as a new userId, resets it automatically. If the error persists, the fallback comes back instead of looping.

    What interviewers listen for
    • A child that throws during render shows the fallback
    • Event handler and async errors aren't caught
    • Siblings outside the error boundary keep working
    • Reports include the component stack, not personal data
    • Reset with resetErrorBoundary or resetKeys

    Likely follow-up: How would you show an error from a failed event handler using the same fallback UI?

  21. 21.What does a reusable portaled modal dialog need beyond positioning?mid

    A portal only changes where the DOM goes. Everything that makes it a dialog is still your job:

    • Name and role. role="dialog", aria-modal="true" and aria-labelledby pointing at its title.
    • Focus. Move focus inside on open, keep Tab cycling inside, and return focus to the opening button on close.
    • Background. Make the rest of the page unreachable while it's open: set inert on the app root, which React 19 accepts as a boolean prop, and lock body scroll.
    • Dismissal. Escape closes it, and with nested dialogs only the top one responds.
    • Events. React events bubble through the React tree, not the DOM tree, so a click inside a portaled dialog reaches onClick handlers on its React ancestors. That surprises click-outside logic.

    The native <dialog> element opened with showModal() gives you the top layer, an inert background and Escape handling for free, and it's often the better base.

    I'd test with the keyboard alone and with VoiceOver or NVDA, including opening one dialog from another.

    What interviewers listen for
    • Dialog role, aria-modal and a labeled title
    • Move, trap and restore focus
    • inert background and scroll lock
    • React events bubble through portals
    • Native <dialog> with showModal()

    Likely follow-up: How would you restore focus if the button that opened the dialog was removed while it was open?

  22. 22.How would you wrap an imperative chart library in a React component?mid

    React renders an empty <div> and hands it to the library through a ref. React never renders children into that div, so the two never fight over the same DOM.

    Creation goes in an effect that runs once: new ChartLib(containerRef.current) after commit, when the element exists. Its cleanup calls chart.destroy(), which removes the library's listeners, timers and canvas. In development, Strict Mode creates, destroys and recreates it on mount, so a leaky destroy() shows up as two charts.

    Updates go in a second effect that depends on data and options and calls the library's update method on the existing instance. Recreating the chart on every change is slower and loses zoom or tooltip state. Because that effect compares object identity, the parent should memoize data and options, or they'll count as changed on every render.

    For size changes, a ResizeObserver created in the first effect and disconnected in its cleanup handles it. useLayoutEffect is only needed when something must be measured before the browser paints.

    function SalesChart({ data, options }) {
      const containerRef = useRef(null);
      const chartRef = useRef(null);
    
      useEffect(() => {
        const chart = new ChartLib(containerRef.current);
        chartRef.current = chart;
        return () => {
          chart.destroy();
          chartRef.current = null;
        };
      }, []);
    
      useEffect(() => {
        chartRef.current?.update({ data, options });
      }, [data, options]);
    
      return <div ref={containerRef} />;
    }
    What interviewers listen for
    • An empty container element through a ref
    • Create once in an effect; destroy() in cleanup
    • A separate effect updates the existing instance
    • Memoize data and options
    • Strict Mode exposes leaky cleanup

    Likely follow-up: How would you expose chart methods such as exportPng() to a parent component?

  23. 23.How would you reconcile an optimistically added comment with the server's response?hard

    React 19's useOptimistic handles the display layer. Inside the form action, addOptimistic(...) shows the comment immediately, styled as pending. When the action finishes, React drops the optimistic layer and shows the real comments state. So on success, the server's comment must be in real state before the action ends, and on failure the pending comment disappears by itself.

    The key is identity. I generate a clientId with crypto.randomUUID() before sending, which the server stores and returns with the real id. That matches the pending item to the saved one, and it dedupes the common race where a WebSocket broadcast of the same comment arrives before the HTTP response. The server can also treat clientId as an idempotency key, so a retry doesn't post twice.

    Failure needs a decision, not a silent disappearance: keep the text with a 'Couldn't post. Retry?' message so nothing the user typed is lost. And since two comments can be in flight at once, every lookup goes by clientId, never by position.

    const [optimistic, addOptimistic] = useOptimistic(
      comments,
      (list, pending) => [...list, pending],
    );
    
    async function postAction(formData) {
      const text = formData.get('text');
      const clientId = crypto.randomUUID();
      addOptimistic({ id: clientId, clientId, text, pending: true });
      try {
        const saved = await api.postComment({ text, clientId });
        startTransition(() => setComments((list) => [...list, saved]));
      } catch {
        setFailed({ clientId, text });
      }
    }
    
    // <form action={postAction}> ... render `optimistic`, not `comments`
    What interviewers listen for
    • useOptimistic shows the pending comment at once
    • Write the saved comment to real state before the action ends
    • A client-generated id links pending and saved items
    • Dedupe WebSocket echoes; make retries idempotent
    • Keep failed text with a retry option

    Likely follow-up: What happens if you call the optimistic setter outside a transition or action?

  24. 24.How would you return useful form validation errors from a React 19 form action?mid

    Validate on the server, because anything the browser checked can be bypassed, and return expected failures as data instead of throwing. With useActionState(createAccount, initialState), the action receives (prevState, formData) and returns the next state, something like { values, errors: { email: 'That email is already registered.' } }. Throwing is for the unexpected, and those errors go to the nearest error boundary.

    In the form, each error sits next to its field: aria-invalid on the input and aria-describedby pointing at the message, so screen readers read it when the field gets focus. isPending disables the button while the action runs.

    Return the submitted values too. React 19 resets uncontrolled fields after a form action completes, even when it returned errors, so without defaultValue={state.values.email} the user's input vanishes.

    Keep messages human and safe: map a duplicate-key database error to 'That email is already registered', and never return stack traces or SQL. Client-side checks like required and type="email" still help for instant feedback, but the server decides.

    // actions.js
    'use server';
    export async function createAccount(prevState, formData) {
      const values = { email: String(formData.get('email') ?? '') };
      if (!values.email.includes('@')) {
        return { values, errors: { email: 'Enter a valid email address.' } };
      }
      if (await emailExists(values.email)) {
        return { values, errors: { email: 'That email is already registered.' } };
      }
      await saveAccount(values);
      return { values: {}, errors: {}, done: true };
    }
    
    // SignupForm.jsx
    'use client';
    import { useActionState } from 'react';
    import { createAccount } from './actions';
    
    export function SignupForm() {
      const [state, formAction, isPending] = useActionState(createAccount, { values: {}, errors: {} });
      return (
        <form action={formAction}>
          <input name="email" defaultValue={state.values.email}
            aria-invalid={Boolean(state.errors.email)} aria-describedby="email-error" />
          <p id="email-error">{state.errors.email}</p>
          <button disabled={isPending}>Create account</button>
        </form>
      );
    }
    What interviewers listen for
    • Validate on the server
    • Return expected errors as state; throw only unexpected ones
    • aria-invalid and aria-describedby on each field
    • Return submitted values for defaultValue
    • No stack traces or raw database errors

    Likely follow-up: After a failed submit, how would you move focus to an error summary?

  25. 25.What should happen to client-side caches when the signed-in account changes?hard

    Everything cached for the previous user has to go before the new user sees a screen. With TanStack Query, queryClient.cancelQueries() stops in-flight requests and queryClient.clear() empties the cache on sign-out or switch. Putting the user id in query keys, like ['orders', userId], adds a second layer: even a stray entry can't be served to the wrong account.

    Late responses are the subtle bug. A slow request started for user A can resolve after B signs in and write A's data into B's screen. Cancelling helps; in hand-written fetch code, checking that the response's user matches the current one is the reliable guard.

    Component state needs a reset too: drafts, selections, open panels. The simplest way is remounting the signed-in tree with key={user.id}, which resets every piece of state below it.

    Don't forget persisted copies: a persisted query cache, drafts in localStorage, IndexedDB and service worker caches. None of this replaces server authorization, which still checks every request against the session.

    What interviewers listen for
    • cancelQueries() and clear() on an account change
    • User id in query keys
    • Drop late responses for the previous user
    • key={user.id} resets component state
    • Clear persisted caches; the server still authorizes

    Likely follow-up: How would you handle a second tab when the user signs out in the first one?

  26. 26.How would you stop an infinite-scroll list from mixing pages from different filters?mid

    Make the filters part of the list's identity. With TanStack Query's useInfiniteQuery, a key like ['products', { category, sort }] means a filter change starts a new list from the first page, and a page still loading for the old key can only land in the old cache entry. Its signal aborts requests nobody needs anymore.

    Without the library: when filters change, reset the pages and the cursor, abort the in-flight request, and ignore any response whose filters don't match the current ones.

    Cursor pagination beats offsets here. With ?offset=40, an item inserted at the top shifts everything down, so the next page repeats an item; a cursor doesn't. If duplicates are still possible, dedupe by id when appending.

    The UI keeps two loading states apart. The first load shows a skeleton in place of the list, while isFetchingNextPage shows a small spinner at the bottom. A failed next page shows a retry button there and keeps the loaded pages. An IntersectionObserver on a sentinel element triggers the next fetch.

    const products = useInfiniteQuery({
      queryKey: ['products', { category, sort }],
      queryFn: ({ pageParam, signal }) =>
        fetchProducts({ category, sort, cursor: pageParam, signal }),
      initialPageParam: null,
      getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
    });
    What interviewers listen for
    • Filters and sort in the query key
    • Reset pages and cursor when filters change
    • Abort or ignore stale responses
    • Cursor pagination; dedupe by id
    • Separate first-load and next-page states

    Likely follow-up: How would you restore the scroll position and loaded pages when the user comes back from a detail page?

  27. 27.How would you protect unsaved changes when a user tries to leave an editor?mid

    First, know whether there's anything to lose. Compare the draft with the last saved version rather than setting a flag on the first keystroke, so undoing an edit clears the warning.

    For in-app route changes, React Router's data routers provide useBlocker(isDirty). When the user clicks a link, blocker.state becomes 'blocked' and I show my own dialog: save, discard or stay. blocker.proceed() continues and blocker.reset() cancels. After a successful save the draft equals the saved version, so the next route change isn't blocked.

    Refreshing, closing the tab or typing a new URL leaves React entirely, so those need a beforeunload listener that calls event.preventDefault(). The browser shows its own generic prompt, ignores custom text, and may skip it if the user never interacted with the page. I add that listener only while the form is dirty.

    When losing the work would really hurt, like a long article, I'd also autosave the draft to localStorage and offer to restore it, because prompts can be dismissed or never shown.

    What interviewers listen for
    • Dirty means the draft differs from the saved version
    • useBlocker for in-app route changes
    • beforeunload for refresh and closing the tab
    • The browser's prompt text can't be customized
    • Autosave drafts when the work is costly

    Likely follow-up: Why shouldn't a beforeunload listener stay attached all the time?

  28. 28.What accessibility work do client-side route changes need in a React app?mid

    A full page load gives screen reader users a lot for free: the new title is announced and reading starts at the top. A client-side route change does none of that. Focus stays on the link that was clicked, or drops to body if that link unmounted, and nothing is announced.

    So on each route change I'd do three things. Update document.title; React 19 hoists a <title> rendered inside the page component into the head. Move focus to the new page's main heading, an h1 with tabIndex={-1} focused in an effect, or announce the change through a polite live region. And handle scroll: new pages start at the top, while back and forward restore the previous position, which React Router's <ScrollRestoration /> does.

    Not every URL change is a page change. Updating a filter in the query string shouldn't pull focus away from the filter control in use.

    Then test with a keyboard and VoiceOver or NVDA: follow a link, and check what's announced and where Tab goes next.

    What interviewers listen for
    • Client-side route changes announce nothing
    • Update the title; React 19 hoists <title>
    • Focus the main heading or use a live region
    • Scroll to top; restore on back and forward
    • Don't move focus for in-page filter changes

    Likely follow-up: What are the trade-offs between moving focus to the heading and announcing through a live region?

  29. 29.How would you diagnose a hydration mismatch that only happens for some users?hard

    'Only some users' points to something that differs between the server and those users' browsers. React 19 logs a diff of the mismatch in the console, so I'd reproduce it and read that.

    The usual suspects:

    • Time zone and locale. A component like OrderTime formats with the server's time zone and locale, then with the user's during hydration. Chrome DevTools' Sensors panel can override time zone and locale to reproduce it.
    • Browser-only values in render: localStorage, window.innerWidth, cookies read only on the client, Math.random() or Date.now().
    • Browser extensions that inject attributes or nodes, like password managers.
    • Invalid HTML, like a <div> inside a <p>, which the browser restructures before React sees it.

    The fixes make the first client render match the server: pass the time zone and locale from the server and format with them, or render a neutral value first and switch to the client-only one in an effect. suppressHydrationWarning is fine on a single timestamp element, never as a blanket fix.

    function OrderTime({ placedAt }) {
      return <time>{new Date(placedAt).toLocaleString()}</time>;
    }
    What interviewers listen for
    • Read React 19's hydration diff
    • Time zone and locale formatting differ
    • Browser-only values during render
    • Extensions and invalid HTML nesting
    • Make the first client render match; no blanket suppression

    Likely follow-up: How would you render a relative time like '5 minutes ago' without a mismatch?

  30. 30.How would you choose between static generation, server rendering and client rendering for a React app?mid

    Per route, based on freshness, personalization, SEO and cost.

    Static generation builds HTML once at build time. It's the cheapest and fastest to serve, from a CDN, and fits pages that are the same for everyone and change rarely: marketing pages, docs, blog posts. With revalidation, like Next.js's revalidate option, static pages refresh every few minutes without a rebuild.

    Server rendering builds HTML on each request. It fits content that must be fresh or personalized but still needs fast first paint or crawlable HTML: search results, product pages with live stock, a signed-in home page. The costs are servers, time to first byte, and caching that must respect personalization.

    Client rendering ships a shell and fetches data in the browser. It suits apps behind a login where SEO doesn't matter and interaction dominates, like an admin tool.

    Most real apps mix them: a static landing page, server-rendered product pages and a client-rendered dashboard. I'd decide per route with TTFB, LCP, INP and hosting cost.

    What interviewers listen for
    • Decide per route
    • Static for shared, rarely changing pages
    • Server rendering for fresh or personalized pages that need SEO
    • Client rendering for logged-in, interactive apps
    • Compare TTFB, LCP, INP and cost

    Likely follow-up: What changes in your caching once a server-rendered page is personalized?

  31. 31.On a product page built with Server Components, how would you decide where to put 'use client'?hard

    Start with everything on the server and add 'use client' only where the browser is needed: state, effects, event handlers or browser APIs. On a product page, the server component reads the product from the database and renders the title, description, images, specs and reviews, and none of that code ships to the browser. Interactive parts, like a quantity stepper, an image carousel or a variant picker, become small client components.

    'use client' marks a module and everything it imports as client code, so putting it high up, on the page layout, drags the whole tree into the bundle. Server components can still be passed into a client component as children and stay on the server.

    Props crossing to the client must be serializable, and they're visible in the page payload. Pass productId, price and stock, not the whole product row with cost price and supplier notes.

    The add-to-cart Server Function re-checks price, stock and the user's session on the server, because the client can send anything.

    What interviewers listen for
    • Server by default; client only for state, effects and events
    • 'use client' pulls in everything the module imports
    • Pass server components as children
    • Serializable, minimal props, because they're visible
    • Server Functions re-validate everything

    Likely follow-up: What can and can't be passed as props from a Server Component to a Client Component?

  32. 32.What should you consider before replacing a local calculation with a Server Function call?hard

    A Server Function call is a network request, a POST under the hood. That brings latency, from milliseconds to seconds on a bad connection, and new failure modes: offline, timeouts, 500s. The UI needs a pending state, through useTransition or useActionState, and an error state.

    It's also a public endpoint. Anyone can call it with any arguments, so it must check authentication, authorization and input, even if its button is hidden.

    I keep calculations local when they're cheap and need no secrets: a cart subtotal while quantities change, input formatting, filtering a list already on the page. A Server Function earns the round trip when it needs trusted data or causes effects: applying a discount code, final pricing with tax rules, writing to the database.

    In Next.js the client sends Server Function calls one at a time, so one call per keystroke queues up and feels slow; debounce, or calculate locally and confirm once on submit. For reads, server components or cached GET routes fit better.

    What interviewers listen for
    • Each call is a network request with latency and failures
    • Pending and error states in the UI
    • A public endpoint: check auth and inputs
    • Keep cheap, non-secret calculations local
    • No per-keystroke calls; prefer server components for reads

    Likely follow-up: How would you show an instant subtotal while still guaranteeing the server's price at checkout?

  33. 33.How would you test that a React search screen ignores stale results?hard

    The bug only appears when responses arrive out of order, so the test must control the order.

    I'd mock the search function so each call returns a promise I resolve by hand, with a small deferred() helper. Enter 'rea', then 'react', which starts two requests. Resolve the second first, with results for 'react', then the first, with results for 'rea', each inside act().

    Then assert that the 'react' results show, the 'rea' results never appear, and the loading indicator is gone. A buggy implementation passes when responses arrive in order and fails here.

    Two more cases: unmounting while a request is pending causes no error or update, and with AbortController the first request's signal is aborted. Aborting alone isn't enough, since a response can resolve just as you abort, so the component should also ignore results for a query that's no longer current. With a debounce, fake timers keep the test fast.

    function deferred() {
      let resolve;
      const promise = new Promise((r) => (resolve = r));
      return { promise, resolve };
    }
    
    test('shows results for the latest query only', async () => {
      const first = deferred();
      const second = deferred();
      searchApi.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise);
    
      render(<Search />);
      const box = screen.getByRole('searchbox');
      fireEvent.change(box, { target: { value: 'rea' } });
      fireEvent.change(box, { target: { value: 'react' } });
    
      await act(async () => second.resolve([{ id: 2, name: 'React docs' }]));
      await act(async () => first.resolve([{ id: 1, name: 'Reading list' }]));
    
      expect(screen.getByText('React docs')).toBeInTheDocument();
      expect(screen.queryByText('Reading list')).not.toBeInTheDocument();
    });
    What interviewers listen for
    • Control each request's promise by hand
    • Resolve the newer request first, then the older one
    • Assert that only the latest results show
    • Check the loading state is correct
    • Cover unmount and abort cases

    Likely follow-up: How would you implement the component so it passes this test?

  34. 34.How would you test a custom Hook that subscribes to an external event source?mid

    I'd swap the real source for a fake I can drive: subscribe(fn) that records listeners and returns an unsubscribe function, an emit(value) method, and a listener count.

    Then I test through the Hook's result with renderHook or a tiny test component:

    • Emit inside act() and assert result.current shows the new value.
    • Call unmount() and assert the listener count is zero.
    • Re-render with a different source and assert the old one has no listeners and the new one has exactly one.
    • Mount and unmount several times; the count shouldn't grow.

    With useSyncExternalStore, two bugs deserve tests. A subscribe created inline changes every render, so React resubscribes each time, visible as listener churn. And getSnapshot must return the same value until the source changes, or React re-renders in a loop.

    I wouldn't assert on internal refs or state, which can change while behavior stays the same.

    function createFakeSource() {
      const listeners = new Set();
      return {
        subscribe(fn) {
          listeners.add(fn);
          return () => listeners.delete(fn);
        },
        emit(value) {
          listeners.forEach((fn) => fn(value));
        },
        get listenerCount() {
          return listeners.size;
        },
      };
    }
    
    test('updates on events and unsubscribes on unmount', () => {
      const source = createFakeSource();
      const { result, unmount } = renderHook(() => useLatestValue(source));
      act(() => source.emit(42));
      expect(result.current).toBe(42);
      unmount();
      expect(source.listenerCount).toBe(0);
    });
    What interviewers listen for
    • A fake source with subscribe, emit and a listener count
    • Assert values through renderHook or a test component
    • Unmounting and source changes remove listeners
    • Repeated mounts don't grow the count
    • Stable subscribe and cached getSnapshot

    Likely follow-up: Why must getSnapshot return a cached value?

  35. 35.How would you type a reusable React list component that renders different record shapes?mid

    Make the component generic over the item type: ListProps<T> has items: readonly T[], plus getKey and renderItem callbacks that each take a T. TypeScript infers T from items, so with items={users} both callbacks know they receive a User, and u.email autocompletes and type-checks.

    getKey is passed in on purpose. Not every record has an id; some use sku, some a composite key. Assuming a field would force any or a cast, and falling back to the array index breaks row state and animations when the list reorders.

    Typing items as any would compile too, but then a renderItem written for products could receive users and nobody would know until runtime.

    Two practical notes. In a .tsx file a generic arrow function needs <T,> so the parser doesn't read it as JSX. And in React 19, ref is an ordinary prop on function components, so a generic list that takes a ref no longer needs the forwardRef cast that used to lose the type parameter.

    type ListProps<T> = {
      items: readonly T[];
      getKey: (item: T) => string | number;
      renderItem: (item: T) => React.ReactNode;
      empty?: React.ReactNode;
    };
    
    function List<T>({ items, getKey, renderItem, empty = null }: ListProps<T>) {
      if (items.length === 0) return <>{empty}</>;
      return (
        <ul>
          {items.map((item) => (
            <li key={getKey(item)}>{renderItem(item)}</li>
          ))}
        </ul>
      );
    }
    
    // T is inferred as User, so u.email autocompletes and type-checks
    <List items={users} getKey={(u) => u.id} renderItem={(u) => u.email} />;
    What interviewers listen for
    • Generic ListProps<T> with renderItem and getKey
    • T is inferred from items
    • Key selection is passed in, not assumed
    • No any: mismatched renderers fail to compile
    • React 19's ref-as-prop keeps generics intact

    Likely follow-up: How would you constrain T so getKey can default to item.id?

  36. 36.How can TypeScript prevent contradictory loading and error flags in React state?mid

    Three independent pieces of state, isLoading, error and data, allow eight combinations, and several are nonsense: loading with an error, or an error next to fresh data. Each one turns into a rendering bug sooner or later.

    A discriminated union allows only the valid ones: { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error }. Data exists only in the success case, so TypeScript won't let you read state.data until you've checked state.status === 'success'. A switch on status narrows each branch, and assigning the leftover value to never in default makes the compiler complain when someone adds a state and forgets to render it.

    Real screens often need one more state: refreshing while still showing the old data. Model it on purpose, as { status: 'refreshing'; data: T } or a separate staleData field, rather than sneaking a boolean back in.

    Libraries already work this way. TanStack Query's result type narrows data once you check isSuccess, for example.

    type RequestState<T> =
      | { status: 'idle' }
      | { status: 'loading' }
      | { status: 'success'; data: T }
      | { status: 'error'; error: Error };
    
    function UserPanel({ state }: { state: RequestState<User> }) {
      switch (state.status) {
        case 'idle':
          return null;
        case 'loading':
          return <Spinner />;
        case 'success':
          return <Profile user={state.data} />;
        case 'error':
          return <ErrorMessage error={state.error} />;
        default: {
          const unhandled: never = state;
          return unhandled;
        }
      }
    }
    What interviewers listen for
    • Separate booleans allow impossible combinations
    • A discriminated union on status
    • data exists only in the success case
    • A never check catches unhandled states
    • Model refreshing with stale data on purpose

    Likely follow-up: How would you write a reducer whose actions move between these states safely?

  37. 37.What would you check when implementing an accessible tabs component in React?hard

    I'd start from the WAI-ARIA tabs pattern, or a library that implements it like Radix UI or React Aria, because the details are easy to get wrong.

    Structure: a role="tablist" with an aria-label; tabs with role="tab", aria-selected and aria-controls pointing at their panels; panels with role="tabpanel" and aria-labelledby pointing back. useId() generates ids that match between server and client.

    Keyboard: only the selected tab is in the Tab order (tabIndex={0}, others -1), a roving tabindex. Left and Right arrows move between tabs and wrap; Home and End jump to the ends. Activation is automatic, where focus selects the tab, or manual, with Enter or Space, which is better when panels load slowly.

    Inactive panels get the hidden attribute unless their state must survive, and a panel with no focusable content gets tabIndex={0} so keyboard users can reach it.

    Then test with a keyboard and a screen reader, which should announce something like 'Billing, tab, 2 of 4, selected'.

    What interviewers listen for
    • Tablist, tab and tabpanel roles linked by ids
    • Roving tabindex: only the selected tab is tabbable
    • Arrow keys, Home and End
    • Choose automatic or manual activation
    • Test with a keyboard and a screen reader

    Likely follow-up: When would you choose manual activation over automatic?

  38. 38.How would you validate accessibility beyond automated tests in a React app?mid

    Automated checks like jest-axe or @axe-core/playwright belong in CI, but they only find what a machine can detect: missing labels, contrast values, invalid ARIA. They can't tell whether focus goes somewhere sensible or whether an error message makes sense.

    For each critical flow, like sign-up or checkout, I'd add manual passes:

    • Keyboard only. Every control reachable with Tab, a visible focus indicator, a logical order, no traps, and Escape closing overlays.
    • Screen readers. VoiceOver with Safari, NVDA with Firefox or Chrome, TalkBack on Android. Are buttons named, errors announced, dialogs labeled?
    • Zoom and reflow. 200% zoom, and a 320-pixel-wide viewport without horizontal scrolling.
    • Preferences. Reduced motion and Windows forced colors.

    I test the dynamic states, not just the first screen: validation errors, loading, empty results, toasts and dialogs. In unit tests, querying by role and name, such as getByRole('button', { name: 'Save' }), keeps markup honest too.

    Where possible, sessions with disabled users or an audit by a specialist find what the team's own testing misses.

    What interviewers listen for
    • axe in CI catches only machine-detectable issues
    • A keyboard-only pass on critical flows
    • Screen readers on several platforms
    • Zoom, reflow, reduced motion and forced colors
    • Test errors, loading, toasts and dialogs

    Likely follow-up: Which accessibility problems can axe never detect?

  39. 39.How should a React UI reflect user permissions without acting as the security layer?mid

    The UI uses permissions to be helpful; the server uses them to decide. I'd load the user's permissions from something like /api/me, such as ['invoice:read', 'invoice:approve'], and use a small can('invoice:approve') helper to hide or disable actions. Disabling with a reason, like 'Only finance managers can approve', often beats hiding.

    Every protected read and write is still checked on the server, which answers 403 when the session lacks the permission. Anyone can call the API directly or flip a flag in DevTools.

    Two mistakes to avoid. Don't send data the user may not see and then hide it in the UI: a salary column hidden with CSS, or filtered out after the fetch, is still in the network response. Filter on the server. And roles change mid-session: when an action returns 403, show a clear message and refetch permissions so the UI catches up.

    Route guards that hide pages a user can't use are fine for UX, as long as nobody mistakes them for protection.

    What interviewers listen for
    • The UI hides or disables actions using permissions
    • The server checks every read and write
    • Never send data just to hide it
    • Handle 403 and refetch permissions
    • Route guards are UX only

    Likely follow-up: Should an unavailable action be hidden or shown disabled, and what decides it?

  40. 40.How would you build a preview for user-written Markdown without opening an XSS hole?hard

    First decide whether raw HTML is allowed in the Markdown. Usually it isn't. react-markdown turns Markdown into React elements without dangerouslySetInnerHTML, doesn't render raw HTML by default, and its default urlTransform blocks dangerous schemes such as javascript:. If you need some HTML, add rehype-raw together with rehype-sanitize and an allowlist schema. With a string renderer like marked, run the output through DOMPurify before it goes anywhere near dangerouslySetInnerHTML.

    Links and images need rules of their own: allow http, https and mailto, add rel="noopener noreferrer nofollow" to user links, and consider proxying images so a tracking pixel can't log every reader's IP address.

    Never evaluate user content as code. MDX from untrusted users is JavaScript, not Markdown.

    The preview must use the same pipeline and rules as the published page, ideally one shared module, or the server rendering both. Otherwise the preview looks safe while the published version runs different code. I'd test both with payloads like <img src=x onerror=alert(1)> and [click](javascript:alert(1)).

    What interviewers listen for
    • react-markdown renders elements, not HTML strings
    • Raw HTML only with rehype-sanitize or DOMPurify
    • Allow only safe URL schemes
    • Never run user-written MDX
    • Preview and published page share one pipeline

    Likely follow-up: Why isn't sanitizing only in the preview enough?

  41. 41.How would you organize a growing React application?hard

    By feature first. Each area, like features/billing or features/orders, holds its own components, hooks, API calls and routes, so a billing change mostly touches one folder. Shared code lives in shared/ui for design-system pieces like Button and Dialog, and in shared/lib for small utilities. Top-level components/ and hooks/ folders stop scaling at a few hundred files, because related code gets scattered.

    Dependency direction is the rule that keeps this healthy: features may import from shared, shared never imports from features, and features don't reach into each other's internals. An ESLint rule like import/no-restricted-paths enforces it.

    Data access goes through each feature's query hooks, such as useInvoices(), rather than fetch calls sprinkled through components. Routes are lazy-loaded per feature, and tests sit beside the code.

    I'd add structure when pain appears, not before. A utils.ts growing past a few hundred lines, or two features importing each other, is the signal to split. A monorepo only makes sense once several apps need the same packages.

    What interviewers listen for
    • Feature folders first
    • Shared UI and utilities kept apart from features
    • Features import shared code, never the reverse
    • Data access through feature query hooks
    • Add structure when real pain appears

    Likely follow-up: How would you stop two features from importing each other's internals?

  42. 42.How would you review a reusable React component's API before publishing it to other teams?mid

    I'd write three or four real usages from actual screens before looking at the implementation, because an awkward API shows up immediately in calling code.

    Then a checklist:

    • Controlled and uncontrolled. A Select should accept value with onChange and also defaultValue, like native inputs, and warn when a consumer switches between the two.
    • Native behavior. Pass ref through, which is just a prop in React 19, and spread remaining props such as aria-*, data-* and className onto the root element so consumers aren't stuck.
    • Naming. Follow DOM conventions: onChange, disabled, id, not isDisabled in one component and disabled in the next.
    • Accessibility. Labels, keyboard support and visible error states built in.
    • Styling hooks. Supported variants and CSS variables, so nobody styles .dropdown > div:nth-child(2) and breaks on the next refactor.

    Every supported combination gets a Storybook story, which doubles as documentation and as interaction tests. Breaking changes go out as a major version with a changelog, plus a codemod when the change is mechanical.

    What interviewers listen for
    • Write real usages first
    • Support controlled and uncontrolled modes
    • Forward ref and spread native props
    • DOM naming and built-in accessibility
    • Stories, semantic versioning and changelogs

    Likely follow-up: How would you deprecate a prop without breaking existing consumers?

  43. 43.How would you deploy a static React app without breaking clients that still have the old HTML open?hard

    Bundlers like Vite put a content hash in each asset name, such as assets/index-4f2a9c.js, which allows a safe policy:

    • Assets: Cache-Control: public, max-age=31536000, immutable, safe because a changed file gets a new name.
    • HTML: Cache-Control: no-cache, so browsers revalidate index.html and find new builds.
    • Order: upload new assets first and switch the HTML last.
    • Old assets stay for days, since open tabs with old HTML still lazy-load old chunks.

    One trap: an SPA fallback that serves index.html for unknown paths must not cover /assets/. Otherwise a missing chunk returns HTML with status 200, and the browser fails with a MIME-type error or Unexpected token '<' instead of a clean 404.

    Rollback then means pointing the HTML back at the previous build, whose assets still exist. I'd test deep links, refreshing a nested route, and a tab left open across a deploy. A service worker needs its own update flow, such as prompting users to reload.

    What interviewers listen for
    • Hashed assets cached for a year as immutable
    • HTML served with no-cache
    • Upload assets first, switch the HTML last
    • Keep old assets; no SPA fallback for /assets/
    • Roll back by repointing the HTML

    Likely follow-up: What would you change if the app also has a service worker?

  44. 44.How would you upgrade a production application from React 18 to React 19?mid

    I'd move to React 18.3 first. It's identical to 18.2 but warns about every API React 19 removes, so running the app and tests on 18.3 produces a to-do list.

    Then the official codemods: npx codemod@latest react/19/migration-recipe handles changes like ReactDOM.render to createRoot and moved act imports, and types-react-codemod fixes TypeScript breakages. Removed APIs to look for: ReactDOM.render and hydrate, findDOMNode, string refs, legacy context, propTypes (now silently ignored) and defaultProps on function components. In TypeScript, useRef needs an argument, and an arrow ref callback that implicitly returns a value now counts as a cleanup function.

    Next, the ecosystem: the router, state library, testing library, component library and, if relevant, the framework version must all support 19.

    Then behavior, not just compilation. Run interaction tests, check SSR and hydration warnings, and watch error reporting, since uncaught render errors now go to window.reportError and the new root options instead of being rethrown. Ship it gradually with monitoring and a quick revert.

    What interviewers listen for
    • Move to 18.3 first for deprecation warnings
    • Run the React 19 codemods and the types codemod
    • Removed APIs like ReactDOM.render, string refs and propTypes
    • Check library and framework support
    • Test behavior, hydration and error reporting; staged rollout

    Likely follow-up: What changed in how React 19 reports errors thrown during rendering?

  45. 45.How would you prepare a React screen for internationalization?mid

    Every user-visible string goes through a library like react-intl, i18next or Lingui, as a complete sentence with placeholders. Concatenating 'Hello ' + name fragments breaks in languages with a different word order. Plurals use ICU syntax, such as {count, plural, one {# message} other {# messages}}, because many languages have more than two plural forms.

    Formatting comes from the locale: Intl.NumberFormat for currency, Intl.DateTimeFormat for dates, Intl.RelativeTimeFormat for '3 days ago', and Intl.ListFormat for 'A, B and C'.

    Layout has to flex. German text often runs about 30% longer than English, so avoid fixed widths. Arabic and Hebrew need dir="rtl" on the document and CSS logical properties like margin-inline-start. Forms shouldn't assume people have a first and a last name.

    With SSR, the server picks the locale from the URL or a cookie, renders with it, and sends the same locale, time zone and messages to the client, or hydration will mismatch.

    I'd test with pseudo-localization, which stretches and accents strings, plus a real right-to-left locale.

    What interviewers listen for
    • Complete messages with placeholders, never concatenation
    • ICU plural rules
    • Intl APIs for numbers, dates and lists
    • Room for longer text; RTL with logical properties
    • Server and client share locale and time zone

    Likely follow-up: How does pseudo-localization help before real translations exist?

  46. 46.How would you choose between CSS Modules, utility classes and CSS-in-JS for a React app?mid

    I'd compare them on what will actually hurt this team and this app.

    • CSS Modules scope class names at build time with no runtime cost, work everywhere including Server Components, and are plain CSS. You do jump between files and invent your own token conventions.
    • Utility classes, mostly Tailwind, give fast iteration and consistent tokens from one config. Variants need helpers like clsx or cva, and the team has to accept long class strings.
    • Runtime CSS-in-JS, like styled-components or Emotion, makes prop-driven styles easy but computes styles while rendering and needs client components in a Server Components app. Zero-runtime options like vanilla-extract or Panda CSS compile to static CSS instead.

    Then the practical checks: SSR setup, style ordering where two systems meet, how theming and dark mode work, and bundle cost.

    I'd build one real feature, a form with validation states plus a dialog, in the top two candidates. Whichever wins, design tokens for color, spacing and type live in one place.

    What interviewers listen for
    • CSS Modules: scoped, no runtime, fine with Server Components
    • Tailwind: fast and consistent; variants need helpers
    • Runtime CSS-in-JS costs render time and needs client components
    • Check SSR, ordering and theming
    • Prototype a real feature; keep one token source

    Likely follow-up: What problems appear when two styling systems coexist during a migration?

  47. 47.How would you migrate a large class-based React feature to Hooks safely?mid

    Behavior tests come first, written with Testing Library so they survive the rewrite. Then I convert one leaf component at a time; classes and function components mix freely, so there's no big-bang rewrite.

    The trap is translating lifecycle methods one by one. A subscription split across componentDidMount, componentDidUpdate and componentWillUnmount becomes one useEffect with its cleanup and the right dependencies, while unrelated work that shared a componentDidMount becomes separate effects.

    Some semantics change:

    • this.setState({ a }) merges into an object; a useState setter replaces the value, so split the state or spread the old value.
    • this.props inside a timeout reads the latest props, while a closure in a function component sees the render it came from. Code that relied on the latest value needs a ref.
    • Instance fields like this.timer become useRef.
    • getDerivedStateFromProps usually becomes a value computed during render, or a reset with key.
    • PureComponent becomes memo.

    Error boundaries stay classes, since React 19 still has no Hook for them.

    What interviewers listen for
    • Behavior tests first, then leaf components
    • Group effects by concern, not by lifecycle method
    • A useState setter replaces; setState merged
    • Closures see their own render; refs hold latest values
    • Error boundaries stay as classes

    Likely follow-up: How would you share Hook-based logic with a class component you can't convert yet?

  48. 48.What would you check before replacing an old global store, such as Redux, with context and Hooks?hard

    First, an inventory: every slice, which components read and write it, how often it changes, and what the middleware does, such as thunks or sagas, logging, persistence with redux-persist, and DevTools time travel. Each needs a replacement or a decision to drop it.

    The big technical difference is subscriptions. useSelector(state => state.cart.count) re-renders a component only when that selected value changes. A context re-renders every consumer whenever its value changes, so moving a frequently updated store into one big context can turn a cheap update into a whole-tree re-render. Split contexts per slice, separate state from dispatch, or use a selector-based store like Zustand or useSyncExternalStore.

    Persisted data needs migrating: users have the old shape in storage, say under persist:root, and the first load after release must convert it rather than crash or wipe settings.

    To migrate safely, I'd add hooks like useCart() that wrap useSelector today, move components onto them, then swap the implementation behind the hook. I'd profile critical interactions before and after.

    What interviewers listen for
    • Inventory slices, consumers, middleware and persistence
    • Context re-renders all consumers; selectors don't
    • Split contexts or use a selector-based store
    • Migrate persisted state on the first load
    • A Hook adapter first, then swap the implementation

    Likely follow-up: When is plain context genuinely enough to replace a global store?

  49. 49.How would you design a React dashboard that receives frequent live updates?hard

    Separate receiving data from rendering it. Hundreds of WebSocket messages a second, each put into React state, means hundreds of renders. I'd write messages into a store outside React, a Map from symbol to latest quote, and flush to the screen every 250 milliseconds or once per animation frame.

    Each widget subscribes only to what it shows, through useSyncExternalStore with a selector or a library like Zustand, so a price change for one stock doesn't re-render the news panel. Rows use memo and id keys, and dense charts use canvas.

    Memory must be bounded: a ring buffer of the last N points per series stops an all-day session from growing forever.

    Connections drop, so plan recovery: reconnect with exponential backoff and jitter, then fetch a snapshot and apply only events with sequence numbers after it, which also reveals gaps. While disconnected, show 'Live updates paused since 10:42' so nobody acts on stale numbers. I'd measure render time and heap size over long runs at realistic rates.

    What interviewers listen for
    • Buffer messages outside React; render on a cadence
    • Widgets subscribe to their own slice
    • Bounded history in a ring buffer
    • Reconnect with backoff; resync from a snapshot and sequence numbers
    • Show a stale-data state; measure long runs

    Likely follow-up: How would you pause work while the dashboard tab is in the background?

  50. 50.How would you approach a frontend system design interview for a collaborative document editor?hard

    I'd start with requirements, which decide the architecture: how many people edit one document at once, offline support, rich text or plain, comments and presence cursors, version history, and permission levels like view, comment and edit.

    Then the core decision: how concurrent edits merge. Operational transformation, as in Google Docs, needs a central server to transform operations. CRDTs like Yjs or Automerge merge on any client, which suits offline use, at some memory and size cost.

    On the client, an editor engine such as ProseMirror, Lexical or Tiptap holds the document state, not React. A keystroke shouldn't re-render the document through React; React renders the toolbar, sidebars and comments around it. Local edits apply instantly, sync over a WebSocket and persist to IndexedDB. Presence travels on a separate, short-lived channel.

    Then failure cases: reconnecting and sending missed updates, permissions changing mid-session, and huge documents. I'd close with accessibility, such as keyboard shortcuts and careful announcement of others' edits, and the metrics I'd watch, like sync latency.

    What interviewers listen for
    • Clarify concurrency, offline, rich text and permissions
    • OT needs a central server; CRDTs merge on any client
    • The editor engine holds document state, not React
    • Local-first edits over WebSocket, stored in IndexedDB
    • Reconnects, permission changes, accessibility and metrics

    Likely follow-up: How would you show presence cursors without storing them in the document?

  51. 51.What is React, and what does it mean that React is declarative?easy

    React is a JavaScript library for building user interfaces out of reusable components. It focuses on the view layer; routing, data fetching and build tooling come from the ecosystem or from a framework like Next.js or React Router.

    Declarative means you describe what the UI should look like for the current state, and React works out how to update the DOM to match. You don't call appendChild or classList.add by hand; you write {isOpen && <Menu />} and change isOpen. The mental model is roughly UI = f(state).

    This makes components predictable, because their output depends only on props and state, and lets you build complex screens by composing small pieces. React also batches and minimizes the DOM work for you. It isn't tied to the browser either: react-dom renders to the DOM, while React Native renders to native mobile views.

    What interviewers listen for
    • A component-based library for building user interfaces
    • Declarative: describe the UI for a state; React updates the DOM
    • UI is a function of props and state
    • Renderer-agnostic: react-dom, React Native

    Likely follow-up: How is React different from a full framework like Angular?

  52. 52.What is the virtual DOM, and how does React use it to update the page?easy

    The virtual DOM is React's in-memory description of the UI: a tree of plain JavaScript objects called React elements, produced when your components render. <button className="primary">Save</button> becomes an object roughly like { type: 'button', props: { className: 'primary', children: 'Save' } }.

    When state changes, React re-renders the affected components to get a new tree, compares it with the previous one (reconciliation), and then applies only the necessary changes to the real DOM in the commit phase.

    The point isn't that the virtual DOM is faster than the DOM: a hand-written, targeted DOM update is always faster. The benefit is that you write simple declarative code that conceptually re-renders everything, while React keeps the actual DOM work small and batched. Internally, React tracks this work in a structure called the Fiber tree.

    What interviewers listen for
    • A lightweight JavaScript object tree describing the UI
    • Each render produces a new tree; React diffs it with the old
    • Only the differences are committed to the real DOM
    • The win is declarative code with minimal DOM work, not raw speed

    Likely follow-up: What heuristics does React use when diffing two trees?

  53. 53.What is JSX, and what does it compile to?easy

    JSX is a syntax extension that lets you write markup-like code inside JavaScript. Browsers can't run it, so a compiler such as Babel, SWC, esbuild or TypeScript turns it into plain function calls. With the modern automatic runtime, a JSX element compiles to a jsx(type, props) call imported from react/jsx-runtime. The older classic transform produced React.createElement(...), which is why old files had to import React.

    Rules worth knowing:

    • It's JavaScript, so attributes are camelCase (className, htmlFor, onClick), and {} embeds any expression, but not statements like if or for.
    • A component returns a single root, so siblings go inside a Fragment.
    • Capitalized tags like <Card /> are components; lowercase tags are DOM elements.
    • Strings embedded with {} are escaped, which protects against XSS.
    // Source
    const el = <Button variant="primary">Save</Button>;
    
    // Output of the automatic JSX runtime (roughly)
    import { jsx as _jsx } from 'react/jsx-runtime';
    const el = _jsx(Button, { variant: 'primary', children: 'Save' });
    What interviewers listen for
    • Syntax extension compiled to plain function calls
    • Automatic runtime: jsx() from react/jsx-runtime; classic: React.createElement
    • camelCase attributes; {} holds expressions, not statements
    • Capitalized names are components; embedded strings are escaped
  54. 54.What is a component in React, and how do props work?easy

    A component is a reusable, self-contained piece of UI. In modern React it's a JavaScript function that receives a single props object and returns JSX. Its name must start with a capital letter, so JSX treats it as a component rather than an HTML tag.

    Props are how a parent passes data and behavior down to a child: strings, numbers, objects, callbacks like onSelect, even other JSX. The special children prop holds whatever you nest between the opening and closing tags, which enables composition such as <Card><Avatar /></Card>.

    Props are read-only. A component must never modify its own props; if a value needs to change over time, it belongs in state, either in this component or in a parent that passes a new value down. Default values are usually written as destructuring defaults, for example function Button({ size = 'md' }).

    What interviewers listen for
    • A function that takes props and returns JSX
    • Component names must be capitalized
    • Props pass data and callbacks from parent to child
    • Props are read-only; children enables composition
  55. 55.What is the difference between props and state?easy

    Props are inputs a component receives from its parent. The component can't change them; it re-renders with new props when the parent re-renders and passes different values.

    State is data a component owns and manages itself, created with useState or useReducer. Updating it through the setter schedules a re-render of that component and its children.

    A useful way to decide:

    • If a value comes from the parent, it's a prop.
    • If it changes over time because of user interaction or async results, and this component owns it, it's state.
    • If it can be calculated from other props or state, it's neither: compute it during render.

    The two connect: a parent's state is often passed to a child as props, together with a callback so the child can ask the parent to update it. That's React's one-way data flow.

    What interviewers listen for
    • Props: passed in by the parent, read-only
    • State: owned by the component, changed through its setter
    • Updating state triggers a re-render
    • Values derivable from props or state should not be state

    Likely follow-up: When would you lift state up?

  56. 56.How does useState work, and why does logging the state right after calling its setter show the old value?easy

    useState(initial) returns a pair: the current value and a setter. React stores the value outside your function, attached to this component's position in the tree, so it survives between renders.

    Calling the setter doesn't change the variable you already have. It queues an update and schedules a re-render; on the next render, useState returns the new value. So after setCount(count + 1), logging count still shows the old value. Each render sees its own snapshot of state, and a const from that render never changes.

    Other details:

    • If the new value is the same as the current one by Object.is, React skips the re-render.
    • For expensive initial values pass a function, useState(() => parse(data)), so it runs only on the first render.
    • When the next value depends on the previous one, pass an updater: setCount(c => c + 1).
    function Counter() {
      const [count, setCount] = useState(0);
    
      function handleClick() {
        setCount(count + 1);
        console.log(count); // still 0 on the first click
      }
    
      return <button onClick={handleClick}>{count}</button>;
    }
    What interviewers listen for
    • Returns the current value and a setter
    • The setter queues a re-render; it does not mutate the variable
    • Each render sees a snapshot of state
    • Same value by Object.is skips the re-render
    • Lazy initializer and updater function forms

    Likely follow-up: What happens if you call setCount(count + 1) three times in one handler?

  57. 57.Explain useEffect. When does it run, and how does the dependency array change that?easy

    useEffect(setup, deps?) lets a component synchronize with something outside React: a subscription, a timer, a network request, a non-React widget. React runs the setup after the render is committed, usually after the browser has painted, so it doesn't block the screen update.

    The dependency array controls when it runs again:

    • No array: after every render.
    • []: only after the first render (mount).
    • [a, b]: after mount, and after any render where a or b changed, compared with Object.is.

    If setup returns a function, that's the cleanup. React calls it before re-running the effect with new dependencies, and when the component unmounts. Every reactive value the effect reads (props, state, functions declared in the component) should be listed; the react-hooks/exhaustive-deps lint rule enforces this. Think "what does this effect stay in sync with", not "which lifecycle moment is this".

    useEffect(() => {
      const id = setInterval(() => setSeconds(s => s + 1), 1000);
      return () => clearInterval(id); // cleanup
    }, []); // reads no reactive values, so no dependencies
    What interviewers listen for
    • Synchronizes a component with an external system
    • Runs after commit, usually after paint
    • No array: every render; []: mount; [a]: when a changes
    • Cleanup runs before the next run and on unmount
    • List every reactive value the effect uses

    Likely follow-up: Why does my effect run twice in development?

  58. 58.Why do list items need a key, and what can go wrong if you use the array index as the key?easy

    When rendering a list, React uses key to match each child with its counterpart from the previous render. Keys tell React which item is which, so when the list changes it can move, insert or remove the right DOM nodes and keep each item's state attached to the correct item.

    With the index as key, identity is tied to position, not to the data. If you insert at the top, delete from the middle, sort or filter, items shift to different indexes. React then reuses the wrong component instances: an uncontrolled input keeps text typed into another row, a checkbox stays checked on the wrong item, and more rows re-render than necessary.

    Good keys are stable and unique among siblings, typically an id from the data. The index is acceptable only for static lists that never reorder or change. Never generate keys during render with Math.random(): every render would remount every item. Keys aren't passed to the component as a prop.

    // Buggy if todos can be reordered, inserted or removed
    {todos.map((todo, i) => <TodoRow key={i} todo={todo} />)}
    
    // Stable identity
    {todos.map(todo => <TodoRow key={todo.id} todo={todo} />)}
    What interviewers listen for
    • Keys give list children a stable identity across renders
    • Index keys tie state to position, not to the item
    • Bugs appear on insert, delete, sort or filter
    • Use stable unique ids; never random keys
    • Keys only need to be unique among siblings
  59. 59.What are the differences between class components and function components, and why did React move to function components?easy

    Class components extend React.Component, keep state in this.state, update it with this.setState, and put side effects in lifecycle methods like componentDidMount, componentDidUpdate and componentWillUnmount. Function components are plain functions that return JSX and use hooks for state and effects.

    Why function components with hooks won:

    • Reusable logic: custom hooks replaced higher-order components and render props, which caused deeply nested "wrapper hell".
    • Related code stays together: one effect holds a subscription and its cleanup, instead of splitting it across three lifecycle methods.
    • No this: no binding event handlers, and no surprises from reading this.props in async callbacks after it changed.
    • Less boilerplate, and better support from tools such as the React Compiler.

    Classes are still supported, and you still need one for an error boundary, because there's no hook equivalent of componentDidCatch. New code should use function components.

    What interviewers listen for
    • Classes: this.state, setState, lifecycle methods
    • Function components: plain functions that use hooks
    • Hooks share logic without wrapper components
    • Effects keep setup and cleanup together; no this binding
    • Classes are still needed for error boundaries
  60. 60.What are hooks, and what are the Rules of Hooks?easy

    Hooks are functions whose names start with use that let function components use React features: state (useState), effects (useEffect), context (useContext), refs and more. They were introduced in React 16.8.

    The rules:

    • Only call hooks at the top level of a component or custom hook: never inside conditions, loops, nested functions, or after an early return.
    • Only call hooks from React functions: components and custom hooks, not plain utility functions or event handlers.

    The reason is that React doesn't identify hooks by name; it relies on call order. On every render it walks through the hook calls in sequence and matches each one to its stored state. If a condition skips one call, every later hook reads the wrong slot. eslint-plugin-react-hooks catches violations. The exception is React 19's use API, which can be called conditionally.

    function Profile({ user }) {
      if (!user) return null; // early return...
      const [open, setOpen] = useState(false); // ...makes this hook conditional: bug
    
      // Fix: call all hooks first, then return early
    }
    What interviewers listen for
    • Functions starting with use for state, effects, context, refs
    • Call them only at the top level, never conditionally
    • Call them only from components or custom hooks
    • React matches hooks to state by call order
    • use is the exception and may be conditional

    Likely follow-up: Why can use be called conditionally when other hooks cannot?

  61. 61.What does "lifting state up" mean, and when do you do it?easy

    When two components need to share the same data or stay in sync, you move that state to their closest common parent. The parent owns the state and passes it down as props, together with callbacks the children call to request changes.

    Classic example: an accordion where only one panel can be open at a time. If each Panel has its own isOpen state, they can't coordinate. Lift it into Accordion as activeIndex, then pass each panel isActive and an onShow callback.

    This keeps a single source of truth: each piece of state lives in exactly one component, and everything else derives from it. The trade-off is that the parent now re-renders on every change, and props may have to travel through several levels. Lift state only as high as it needs to go; if you're lifting very high just to reach distant components, consider composition, context or a store.

    function Accordion() {
      const [activeIndex, setActiveIndex] = useState(0);
      return (
        <>
          <Panel title="About" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)} />
          <Panel title="Pricing" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)} />
        </>
      );
    }
    What interviewers listen for
    • Move shared state to the closest common parent
    • Pass the value and change callbacks down as props
    • Keeps a single source of truth
    • Lift only as high as needed

    Likely follow-up: What is prop drilling, and how do you avoid it?

  62. 62.What is one-way data flow in React, and how does a child component send data back to its parent?easy

    In React, data flows down the tree: parents pass values to children through props, and children can't modify those props. When state changes, the new values flow down on the next render. This makes it easy to trace where a value comes from: follow the props up to the component that owns the state.

    A child communicates upward by calling a callback that the parent passed as a prop, such as onChange(newValue) or onDelete(id). The child only reports what happened; the parent owns the state and decides how to update it.

    Compared with two-way binding, like [(ngModel)] in Angular, this is more explicit: a controlled input needs both value and onChange. The payoff is predictability. There's exactly one place that can change a given piece of state, which makes updates easier to reason about and debug.

    What interviewers listen for
    • Data flows down through read-only props
    • Children notify parents by calling callback props
    • The state owner decides how to update
    • More explicit and predictable than two-way binding
  63. 63.What exactly is the difference between useMemo and useCallback, and how could you write one using the other?mid

    Both cache something between renders and recompute only when a dependency changes (compared with Object.is). The difference is what they cache:

    • useMemo(() => compute(a, b), [a, b]) calls your function and caches its return value.
    • useCallback(fn, [a, b]) caches the function itself, without calling it.

    So useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

    Use useMemo to skip an expensive calculation, like filtering a large list, or to keep an object or array referentially stable. Use useCallback when a function is passed to a memo-wrapped child or used as a dependency of an effect or another hook. Without a consumer that cares about identity, useCallback achieves nothing: the inline function is still created on every render, React just hands back the cached one. Both are performance hints, so your code must still behave correctly without them.

    // Caches a value
    const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
    
    // Caches a function
    const handleSelect = useCallback(id => setSelectedId(id), []);
    
    // The same thing written with useMemo
    const handleSelect2 = useMemo(() => id => setSelectedId(id), []);
    What interviewers listen for
    • useMemo caches a computed value
    • useCallback caches a function reference
    • useCallback(fn, deps) equals useMemo(() => fn, deps)
    • Only useful when identity or cost matters
    • Performance hints: code must work without them
  64. 64.You wrapped a child in React.memo, but it still re-renders every time the parent does. What are the likely causes?mid

    memo skips a re-render only when every prop is equal to its previous value by Object.is, which is a shallow reference comparison. The usual culprit is a prop recreated on every parent render:

    • inline objects or arrays: style={{ color: 'red' }}, options={[1, 2]},
    • inline functions: onClick={() => select(id)},
    • JSX passed as children, since each render creates new element objects.

    Each render makes a new reference and {} !== {}, so the comparison fails. Fixes: hoist constants outside the component, wrap objects in useMemo and callbacks in useCallback, or pass primitives instead of objects.

    Also check what memo can't block: the child still re-renders when its own state changes or when a context it reads changes. You can pass a custom comparison function as the second argument, but it must compare every prop, including functions, or you'll get stale closures. The React Compiler can handle most of this automatically.

    const PriceChart = memo(function PriceChart({ data, options, onPointClick }) {
      /* expensive rendering */
    });
    
    function Dashboard({ data }) {
      // A new object and a new function on every render: memo never skips
      return <PriceChart data={data} options={{ grid: true }} onPointClick={p => log(p)} />;
    }
    What interviewers listen for
    • memo shallowly compares props with Object.is
    • Inline objects, arrays, functions and JSX break it
    • Stabilize with hoisting, useMemo and useCallback
    • Own state and context changes still re-render
    • Custom comparators risk stale closures
  65. 65.What is the difference between useRef and useState? When would you keep a value in a ref instead of state?mid

    Both persist a value across renders, but they behave differently when the value changes:

    • useState gives you a value and a setter; updating it triggers a re-render, and the new value appears in the next render's snapshot.
    • useRef gives you a mutable object, { current }, that stays the same object for the component's lifetime. Assigning ref.current = x takes effect immediately and does not re-render.

    Use state for anything that affects what's on screen. Use a ref for values that event handlers or effects need but rendering doesn't: an interval or timeout id, an AbortController, the previous value of a prop, a flag like "already sent analytics", or a DOM node via ref={inputRef}.

    Avoid reading or writing ref.current during render (lazy initialization aside). React doesn't know when a ref changes, so UI that depends on it can silently get out of sync.

    function Stopwatch() {
      const [elapsed, setElapsed] = useState(0); // shown on screen: state
      const intervalRef = useRef(null);          // just an id: ref
    
      function start() {
        intervalRef.current = setInterval(() => setElapsed(e => e + 1), 1000);
      }
      function stop() {
        clearInterval(intervalRef.current);
      }
    
      return <Timer seconds={elapsed} onStart={start} onStop={stop} />;
    }
    What interviewers listen for
    • Both persist values across renders
    • Setting state re-renders; mutating a ref does not
    • Ref updates are immediate
    • Refs suit timer ids, DOM nodes and non-visual values
    • Don't read or write refs during render
  66. 66.What is state batching, and why would you use a functional update like setCount(c => c + 1)?mid

    Batching means React groups several state updates made in the same event into a single re-render. Since React 18 with createRoot, this automatic batching also applies inside promises, setTimeout and native event handlers, not only in React event handlers. It avoids wasted renders and half-updated UI.

    Batching interacts with snapshots. Inside one handler, count is fixed for that render, so calling setCount(count + 1) three times queues "set to 1" three times, and the result is 1. An updater function is queued instead of a value; React applies the queue in order, passing each updater the result of the previous one, so three setCount(c => c + 1) calls produce 3.

    Use the updater form whenever the next state depends on the previous one, especially in async callbacks, intervals or effects where the captured value may be stale. If you really need an update applied synchronously, for example before measuring the DOM, flushSync from react-dom exists, but it's rarely needed.

    function addThreeBuggy() {
      setCount(count + 1);
      setCount(count + 1);
      setCount(count + 1); // 0 -> 1: every call uses the same snapshot
    }
    
    function addThree() {
      setCount(c => c + 1);
      setCount(c => c + 1);
      setCount(c => c + 1); // 0 -> 3: each updater gets the latest value
    }
    What interviewers listen for
    • Several updates in one event cause one re-render
    • Automatic batching everywhere since React 18 with createRoot
    • State is a fixed snapshot within a render
    • Updater functions are queued and applied in order
    • Use updaters when next state depends on previous state

    Likely follow-up: What does flushSync do, and when is it needed?

  67. 67.What is prop drilling, and what are the ways to avoid it?easy

    Prop drilling is passing props through several layers of components that don't use them, only so that a deeply nested component can receive them. It makes the intermediate components noisy, couples them to data they don't care about, and makes refactoring painful.

    Ways to avoid it, roughly in the order I'd try them:

    • Composition: pass JSX through children or other props, so the component that owns the data renders the deep component directly, e.g. <Layout sidebar={<UserMenu user={user} />} />. This often removes the drilling entirely.
    • Context: for data many components need at different depths, like the theme, locale or current user.
    • A store like Zustand, Redux Toolkit or Jotai for frequently updated global client state, and TanStack Query or similar for server data, which any component can read directly.

    Passing a prop through two or three levels is fine and explicit; it's only a problem when it becomes deep and widespread.

    What interviewers listen for
    • Passing props through components that do not use them
    • Try composition with children or element props first
    • Context for widely needed, rarely changing data
    • Stores or query libraries for global or server state
    • Shallow drilling is fine and explicit
  68. 68.How do you create and consume a Context with useContext? What happens if there is no matching provider?mid

    Three steps:

    • Create it: const ThemeContext = createContext('light'). The argument is the default value.
    • Provide a value above the consumers. In React 19 you can render the context itself as the provider, <ThemeContext value={theme}>; older versions use <ThemeContext.Provider value={theme}>.
    • Read it with useContext(ThemeContext) in any descendant.

    useContext returns the value of the closest provider above the calling component, so nested providers can override the value for a subtree. If there's no provider above, it returns the default passed to createContext, which never changes. A common pattern is a null default plus a custom hook that throws a helpful error when the provider is missing.

    When the provider's value changes, every consumer re-renders, so avoid passing a fresh object on every render. React 19 can also read context with use(ThemeContext), which, unlike useContext, may be called inside conditions.

    const ThemeContext = createContext(null);
    
    export function useTheme() {
      const value = useContext(ThemeContext);
      if (value === null) throw new Error('useTheme must be used inside ThemeProvider');
      return value;
    }
    
    export function ThemeProvider({ children }) {
      const [theme, setTheme] = useState('light');
      const value = useMemo(() => ({ theme, setTheme }), [theme]);
      return <ThemeContext value={value}>{children}</ThemeContext>;
    }
    What interviewers listen for
    • createContext(default), provide a value, read with useContext
    • Reads the closest provider above the component
    • No provider: returns the default value
    • React 19: <Context value> works as the provider
    • Value changes re-render every consumer

    Likely follow-up: What are the performance pitfalls of Context?

  69. 69.Is React Context a state management solution? How does it compare with Redux or Zustand?mid

    Strictly speaking, Context is a dependency injection mechanism: it passes a value down the tree without props. It doesn't manage anything itself; the state still lives in useState or useReducer in the provider component. Context plus useReducer gives you a small store, but with limits:

    • Every consumer re-renders when the value changes. There are no selectors, so a component can't subscribe to just one slice.
    • There's no built-in middleware, devtools or persistence.

    Redux Toolkit provides a single store with actions, reducers, selectors, middleware and excellent devtools, including time-travel debugging. It suits large apps and teams that want strict conventions. Zustand is a minimal hook-based store with selector subscriptions and very little boilerplate. With both, a component re-renders only when the data it selects changes.

    My rule of thumb: Context for low-frequency values like theme, locale or auth; a store for frequently updated, widely shared client state; and neither for server data, which belongs in TanStack Query or a framework's loaders.

    What interviewers listen for
    • Context is dependency injection, not a store
    • State still lives in useState/useReducer in the provider
    • No selectors: all consumers re-render on change
    • Redux Toolkit: structure and devtools; Zustand: minimal with selectors
    • Server data belongs in a query library
  70. 70.When would you use useReducer instead of useState?mid

    useReducer(reducer, initialState) returns [state, dispatch]. Instead of setting state directly, event handlers dispatch actions that describe what happened, like { type: 'added', text }, and a pure reducer(state, action) function computes the next state.

    I reach for it when:

    • several pieces of state change together, e.g. status, data and error for a request,
    • the next state depends on the previous one in non-trivial ways,
    • many event handlers update the same state and I want the update logic in one place,
    • I want to unit test the logic in isolation, since a reducer is a plain function.

    dispatch also has a stable identity, so it can go through context or to memoized children without useCallback. For simple independent values, like a toggle or a single input, useState is shorter and clearer. The reducer must be pure and return new objects instead of mutating; in development, StrictMode calls it twice to surface impurity.

    function reducer(todos, action) {
      switch (action.type) {
        case 'added':
          return [...todos, { id: action.id, text: action.text, done: false }];
        case 'toggled':
          return todos.map(t => (t.id === action.id ? { ...t, done: !t.done } : t));
        default:
          throw new Error('Unknown action: ' + action.type);
      }
    }
    
    // In the component
    const [todos, dispatch] = useReducer(reducer, []);
    // In a handler: dispatch({ type: 'toggled', id: todo.id });
    What interviewers listen for
    • Dispatch actions; a pure reducer computes the next state
    • Good for related state that changes together
    • Centralizes update logic and is easy to unit test
    • dispatch has a stable identity
    • Prefer useState for simple independent values
  71. 71.How would you build a form with several inputs in React and handle its submission?easy

    The most common pattern is controlled inputs: keep the values in state, set each input's value from state, and update it in onChange. With several fields, one state object plus a single change handler keyed by the input's name keeps it short.

    For submission, handle onSubmit on the <form> element rather than a button's click, so pressing Enter works too. Call e.preventDefault() to stop the browser's full-page submission, then validate and send the data.

    Details worth mentioning:

    • Checkboxes use checked instead of value, and you read e.target.checked.
    • Give every input a label, linked with htmlFor and id, for accessibility.
    • Controlled inputs re-render on every keystroke, so for large forms libraries like React Hook Form use uncontrolled inputs, often with a schema validator like Zod.

    In React 19 you can also pass a function to the form's action prop; it receives the FormData and React tracks the pending state for you.

    function SignupForm({ onSignup }) {
      const [form, setForm] = useState({ email: '', password: '' });
      const handleChange = e => setForm(f => ({ ...f, [e.target.name]: e.target.value }));
    
      return (
        <form onSubmit={e => { e.preventDefault(); onSignup(form); }}>
          <input name="email" value={form.email} onChange={handleChange} />
          <input name="password" type="password" value={form.password} onChange={handleChange} />
          <button>Sign up</button>
        </form>
      );
    }
    What interviewers listen for
    • Controlled inputs: value from state, update in onChange
    • One handler using the input name for many fields
    • Handle onSubmit and call e.preventDefault()
    • Checkboxes use checked; label every input
    • Large forms: React Hook Form or React 19 form actions
  72. 72.Why should you never mutate state directly, and how do you correctly update an object or array held in state?easy

    React decides whether state changed by comparing the old and new values with Object.is. If you mutate an object and pass the same reference back, as in user.name = 'Ada'; setUser(user), React sees no change and can skip the re-render. Even when something else triggers a render, mutation breaks everything that relies on comparing references, such as memo, useMemo and effect dependencies, and it corrupts the previous render's snapshot.

    Treat state as immutable and create new values instead:

    • Objects: setUser({ ...user, name: 'Ada' }). Spread is shallow, so copy each nested level you change.
    • Arrays: add with [...items, item], remove with filter, update with map returning a new object for the changed item.
    • Sorting or reversing: copy first, [...items].sort(), or use toSorted().

    push, splice, sort, reverse and direct assignment all mutate in place. For deeply nested updates, Immer lets you write mutating-style code that produces immutable copies.

    What interviewers listen for
    • React compares state by reference with Object.is
    • Mutating and setting the same object can skip the render
    • Create new objects and arrays with spread, map, filter
    • Avoid push, splice, sort on state; copy first
    • Immer simplifies deeply nested immutable updates
  73. 73.How do class lifecycle methods like componentDidMount and componentWillUnmount map to hooks?mid

    The rough mapping:

    • constructor and initial state: useState(initial) or a lazy initializer.
    • componentDidMount: useEffect(() => { ... }, []).
    • componentDidUpdate: useEffect with the relevant dependencies (which also runs after mount).
    • componentWillUnmount: the cleanup function returned from an effect.
    • shouldComponentUpdate or PureComponent: memo.
    • getDerivedStateFromProps: usually compute the value during render, or reset with a key.
    • componentDidCatch and getDerivedStateFromError: no hook equivalent; you still need a class error boundary.

    But I'd stress that effects aren't lifecycle methods. An effect describes how to start and stop synchronizing with something, based on its dependencies. Instead of spreading one subscription across mount, update and unmount methods, a single effect sets it up, cleans it up, and re-runs when the values it uses change. Thinking in lifecycles is how people end up with missing dependencies and stale data.

    What interviewers listen for
    • Mount: effect with []; unmount: the effect cleanup
    • Update: effect with dependencies
    • shouldComponentUpdate maps to memo
    • Error boundaries still require classes
    • Effects synchronize by dependencies, not lifecycle moments
  74. 74.When a parent component re-renders, do its children re-render too? How can you avoid unnecessary child renders?mid

    Yes. By default, when a component renders, React re-renders every component it renders, recursively, whether or not their props changed. React doesn't compare props unless you opt in. Usually that's fine: renders are cheap, and React only touches the DOM where the output actually differs.

    When a child is expensive, the options are:

    • Move state down: if only a small part of the UI uses some state, put that state in a smaller component so fewer components re-render.
    • Lift content up as children: when a component re-renders because of its own state, the children elements it received from its parent are the same objects as before, so React skips re-rendering them.
    • memo the expensive child and keep its props referentially stable with useMemo and useCallback.
    • Split contexts, so an update only reaches the components that need it.

    Measure first with the React DevTools Profiler. With the React Compiler enabled, much of this memoization happens automatically.

    What interviewers listen for
    • Children re-render by default when a parent renders
    • Re-rendering is not the same as updating the DOM
    • Move state down or pass content as children
    • memo plus stable props for expensive children
    • Profile before optimizing

    Likely follow-up: How would you find out which components re-render and why?

  75. 75.A React page feels slow. How would you find and fix the performance problem?hard

    First measure, don't guess. The React DevTools Profiler shows which components render, how often and how long they take; the browser's Performance panel shows long tasks, layout thrashing and network waterfalls. Profile a production build, because development mode is much slower.

    Then fix what the profile shows:

    • Too many re-renders: move state closer to where it's used, pass content as children, split contexts, memo expensive children with stable props, or enable the React Compiler.
    • Expensive renders: useMemo heavy calculations and virtualize long lists.
    • Updates blocking input: mark non-urgent updates with startTransition or useDeferredValue so typing stays responsive.
    • Slow loading: code-split routes and heavy widgets with lazy, trim dependencies, optimize images, and remove request waterfalls by fetching in parallel or on the server.
    • Unneeded effects that set state and cause extra render passes.

    Afterwards, verify the improvement with the same measurement, and watch real-user metrics like INP and LCP.

    What interviewers listen for
    • Profile first: React DevTools Profiler and Performance panel
    • Measure against a production build
    • Cut re-renders: colocate state, split context, memo
    • Transitions for non-urgent work; virtualize long lists
    • Code-split and remove request waterfalls

    Likely follow-up: How can you tell why a particular component re-rendered?

  76. 76.What are Fragments, and why would you use them?easy

    A component has to return a single root, but wrapping siblings in an extra div adds meaningless nodes to the DOM. A Fragment groups children without creating any DOM element. You write it as <>...</> or <Fragment>...</Fragment>.

    Why it matters:

    • Valid HTML: a component that returns several table cells, list items or dt/dd pairs can't wrap them in a div without producing invalid markup.
    • Layout: extra wrappers break CSS flexbox and grid layouts, where the styled items must be direct children of the container.
    • A cleaner, shallower DOM.

    The short syntax can't take any attributes. When you render fragments in a list, use the explicit form so you can give each one a key: <Fragment key={item.id}>. The key is the only attribute you'd normally pass to a Fragment.

    What interviewers listen for
    • Groups children without adding a DOM node
    • Short syntax <>...</> or explicit <Fragment>
    • Keeps tables, lists and flex or grid layouts valid
    • Use <Fragment key> when mapping over a list
  77. 77.What are the common ways to render something conditionally in React, and what is the gotcha with &&?easy

    JSX is just JavaScript, so you use ordinary JavaScript:

    • if with an early return for whole-component branches: if (!user) return <Login />.
    • Ternary for either/or inline: {isLoading ? <Spinner /> : <Table />}.
    • Logical && to render something or nothing: {hasError && <Alert />}.
    • Return null to render nothing at all.
    • A variable or lookup object for many cases, instead of nested ternaries.

    The && gotcha: a && b evaluates to a when a is falsy. React renders nothing for false, null and undefined, but it does render the number 0. So {items.length && <List />} shows a stray "0" when the list is empty. Make the left side a real boolean, items.length > 0 && ..., or use a ternary.

    Also remember that switching between different component types at the same position unmounts one and mounts the other, so its state is lost.

    // Renders "0" when there are no messages
    {messages.length && <Badge count={messages.length} />}
    
    // Renders nothing when there are no messages
    {messages.length > 0 && <Badge count={messages.length} />}
    What interviewers listen for
    • Use if/early return, ternaries, && or null
    • a && b returns a when it is falsy
    • 0 renders as text; false, null, undefined do not
    • Use items.length > 0 && or a ternary
  78. 78.Why does React favor composition over inheritance? Give examples of composition in practice.easy

    In React you build complex components by combining smaller ones rather than extending base component classes. The React team has long said they haven't found use cases where they'd recommend component inheritance hierarchies, and in practice you almost never see class FancyButton extends Button.

    Composition tools:

    • children: generic containers like Card, Modal or Layout don't know their content ahead of time; they render whatever is nested inside them.
    • Named slots: props that accept JSX, e.g. <Layout header={<Nav />} sidebar={<Filters />} />.
    • Specialization by wrapping: a DangerButton that renders <Button variant="danger" {...props} />.
    • Logic reuse through custom hooks instead of base classes.

    Composition keeps components loosely coupled and their relationships explicit. It also has practical wins: passing JSX as children avoids prop drilling, because the owner renders the deep component directly, and a component re-rendering because of its own state doesn't re-render the children it was handed.

    What interviewers listen for
    • Build UIs by combining components, not extending them
    • children for generic containers
    • JSX props act as named slots
    • Specialize by wrapping; share logic with hooks
    • Composition also reduces prop drilling and re-renders
  79. 79.Compare higher-order components, render props and custom hooks as ways to share logic between components.mid

    All three solve "reuse stateful logic", and they arrived in roughly this order:

    • A higher-order component is a function that takes a component and returns a new one with extra props, like withAuth(Dashboard) or the old Redux connect. Downsides: extra wrapper layers, props whose origin is unclear, and name collisions when several HOCs are stacked.
    • Render props: a component receives a function and calls it with its data to decide what to render. More explicit, but nesting several produces a pyramid of callbacks.
    • Custom hooks: const user = useUser(). No extra components in the tree, explicit inputs and outputs, and easy to compose and test.

    Today I default to custom hooks. HOCs still appear in libraries for cross-cutting wrappers, and render props or function-as-children remain useful when a component should let its caller control what gets rendered with its data, such as a virtualized list's row renderer.

    // Higher-order component
    const DashboardWithUser = withUser(Dashboard);
    
    // Render prop
    <UserLoader render={user => <Dashboard user={user} />} />
    
    // Custom hook
    function Dashboard() {
      const user = useUser();
      return <h1>Hi, {user.name}</h1>;
    }
    What interviewers listen for
    • HOC: a function wrapping a component to inject props
    • Render props: a component calls a function prop to render
    • Both add nesting and indirection
    • Custom hooks share logic without extra tree nodes
    • Render props still suit delegating what to render
  80. 80.Walk me through writing a useDebouncedValue custom hook for a search input.mid

    The hook keeps its own debounced state. Whenever value changes, the effect starts a timer that copies the value into that state after delay milliseconds.

    The key is the cleanup. If value changes again before the timer fires, React runs the previous effect's cleanup first, which clears the pending timer, so only the last value within the window gets through. The cleanup also clears the timer on unmount, so nothing updates after the component is gone.

    Design points I'd mention:

    • It's named use... and calls hooks at the top level, so it follows the Rules of Hooks.
    • Both value and delay are dependencies, so changing the delay works correctly.
    • Every component that calls it gets independent state.
    • The input stays controlled by the raw query, so typing is instant; only debouncedQuery drives the fetch or filter.

    Unlike useDeferredValue, this uses a fixed delay, which is what you want when the goal is fewer network requests rather than smoother rendering.

    function useDebouncedValue(value, delay = 300) {
      const [debounced, setDebounced] = useState(value);
    
      useEffect(() => {
        const id = setTimeout(() => setDebounced(value), delay);
        return () => clearTimeout(id);
      }, [value, delay]);
    
      return debounced;
    }
    
    // Usage
    const debouncedQuery = useDebouncedValue(query, 400);
    What interviewers listen for
    • State plus an effect that schedules a timeout
    • Cleanup clears the pending timer on change and unmount
    • Both value and delay are dependencies
    • Each caller gets independent state
    • Fixed delay suits network calls; useDeferredValue suits rendering
  81. 81.What happens to a React app when a component throws an error during rendering, and how do you contain the damage?mid

    If nothing catches it, React unmounts the entire root and the user sees a blank page. That's deliberate: leaving corrupted UI on screen, like a checkout showing the wrong total, is considered worse than removing it.

    To contain it you use error boundaries: components that catch errors thrown while rendering the tree below them and render a fallback instead. They're still class components: static getDerivedStateFromError switches to the fallback, and componentDidCatch logs the error. Many teams use the react-error-boundary package, which adds reset support. In React 19, createRoot also accepts onCaughtError and onUncaughtError options for centralized reporting.

    Place boundaries deliberately: one near the root as a last resort, one per route, and around independent widgets so a broken sidebar doesn't take down the whole page. Boundaries don't catch errors in event handlers or asynchronous code, so handle those with try/catch.

    class ErrorBoundary extends React.Component {
      state = { hasError: false };
      static getDerivedStateFromError() {
        return { hasError: true };
      }
    
      componentDidCatch(error, info) {
        logToService(error, info.componentStack);
      }
    
      render() {
        return this.state.hasError ? this.props.fallback : this.props.children;
      }
    }
    What interviewers listen for
    • An uncaught render error unmounts the whole root
    • Error boundaries catch render errors in their subtree
    • Class with getDerivedStateFromError and componentDidCatch
    • Place boundaries at app, route and widget level
    • Event handler and async errors need try/catch
  82. 82.What is code splitting, and how do you implement it in a React app?mid

    Code splitting breaks the JavaScript bundle into smaller chunks that load on demand, so users download only the code for what they're looking at. It improves initial load and time to interactive.

    The mechanism is dynamic import(), which bundlers like Vite and webpack turn into a separate chunk. In React you combine it with lazy and <Suspense>: const Settings = lazy(() => import('./Settings')) loads the chunk the first time Settings renders, while the nearest Suspense boundary shows a fallback.

    Where to split:

    • Routes first: each page becomes its own chunk. Frameworks like Next.js do this automatically.
    • Heavy, rarely used components: rich-text editors, charts, maps, big modals.
    • Large libraries needed in one place, which you can import() inside an event handler.

    Watch out for over-splitting, which adds request overhead, and for spinners on every click: you can preload a chunk, for example on hover, by calling the same import() early. Wrap lazy areas in an error boundary, since loading a chunk can fail.

    What interviewers listen for
    • Split the bundle into chunks loaded on demand
    • Dynamic import() with lazy and Suspense
    • Split by route first, then heavy components
    • Preload likely chunks; handle load failures
    • Avoid splitting too granularly
  83. 83.What are synthetic events in React, and how does React event handling differ from native DOM events?mid

    React passes your handlers a SyntheticEvent: a cross-browser wrapper around the native event with the familiar interface, such as e.target, e.preventDefault() and e.stopPropagation(). The underlying event is available as e.nativeEvent.

    Differences from plain DOM handling:

    • Handlers are camelCase props that take functions, onClick={handleClick}, not strings.
    • You must call e.preventDefault(); returning false does nothing.
    • React uses event delegation: rather than a listener on every element, it listens at the root container (since React 17; before that, on document) and dispatches through the component tree.
    • Propagation follows the React tree, so an event inside a portal bubbles to the portal's React parent even though its DOM node lives elsewhere.
    • onChange fires on every keystroke, like the native input event, not only when the field loses focus.
    • Event pooling was removed in React 17, so reading e inside async code is safe.

    Capture-phase handlers use a Capture suffix, like onClickCapture.

    What interviewers listen for
    • Cross-browser wrapper; the original is e.nativeEvent
    • camelCase props taking functions; call preventDefault() explicitly
    • Delegated at the root container since React 17
    • Propagation follows the React tree, including portals
    • onChange fires on every keystroke
  84. 84.Why does this effect cause an infinite loop of requests, and how do you fix it?mid

    params is a new object on every render, and effect dependencies are compared with Object.is. Two different objects are never equal, so the effect runs after every render. Each run fetches and calls setData with a new array, which triggers another render, which creates another params object, and so on forever.

    Fixes, best first:

    • Depend on primitives: create the object inside the effect and list [query] as the dependency.
    • If the object is needed elsewhere too, memoize it: useMemo(() => ({ q: query, limit: 20 }), [query]).
    • Move values that never change outside the component.

    The same trap appears with functions declared in the component body and used in the effect, and with an effect that updates state it also depends on, like setCount(count + 1) with [count]. Don't silence the lint rule to stop the loop; restructure so the dependencies are honest. While you're there, add a cleanup that ignores stale responses.

    function Results({ query }) {
      const [data, setData] = useState([]);
      const params = { q: query, limit: 20 };
    
      useEffect(() => {
        fetchResults(params).then(setData);
      }, [params]);
    
      return <List items={data} />;
    }
    What interviewers listen for
    • Objects and functions are recreated on every render
    • Dependencies compare with Object.is, so the effect always re-runs
    • Setting state in the effect triggers the next render
    • Build the object inside the effect or depend on primitives
    • Don't suppress the exhaustive-deps lint rule
  85. 85.What is a stale closure in React? Explain why this counter gets stuck at 1 and how to fix it.hard

    Every render creates new functions that close over that render's props and state. A stale closure happens when a function created during an old render keeps running later and still reads that old snapshot.

    Here the effect runs only once, so the interval callback captured count from the first render, where it's 0. Every tick calls setCount(0 + 1): the display goes to 1 and stays there.

    Fixes:

    • Use an updater: setCount(c => c + 1) always receives the latest state, so the effect no longer needs count at all.
    • Add the value to the dependencies, so the effect re-subscribes with fresh values; here that restarts the interval on every change.
    • For a long-lived listener that must always see the latest props, keep the latest callback in a ref, or use useEffectEvent, added in React 19.2.

    The exhaustive-deps lint rule flags most of these. The same bug shows up with useCallback and useMemo that have missing dependencies.

    function Timer() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const id = setInterval(() => {
          setCount(count + 1);
        }, 1000);
        return () => clearInterval(id);
      }, []);
    
      return <p>{count}</p>;
    }
    What interviewers listen for
    • Functions capture props and state from the render that created them
    • Long-lived callbacks can keep reading an old snapshot
    • Fix with an updater: setCount(c => c + 1)
    • Or add dependencies, or read the latest value from a ref
    • The exhaustive-deps lint rule catches most cases
  86. 86.Explain the difference between client-side rendering, server-side rendering and static site generation. What is hydration?mid

    They differ in where and when the HTML is produced:

    • CSR: the server sends a nearly empty HTML shell plus a JavaScript bundle, and React builds the UI in the browser. Simple to host and fine for app-like dashboards, but nothing useful shows until the JavaScript loads, and crawlers see little content.
    • SSR: the server renders components to HTML on each request, so content appears quickly and SEO works. It costs server work per request, and the page isn't interactive until its JavaScript loads.
    • SSG: HTML is rendered at build time and served from a CDN. Fastest and cheapest; ideal for docs, blogs and marketing pages, but content is only as fresh as the last build.

    Hydration follows SSR or SSG: React runs in the browser, renders the same components, and attaches event handlers and state to the existing HTML instead of recreating it, using hydrateRoot. The client's first render must match the server HTML. With Suspense, React can stream HTML and hydrate parts of the page independently.

    What interviewers listen for
    • CSR: the UI is built in the browser from JavaScript
    • SSR: HTML rendered on the server for each request
    • SSG: HTML rendered at build time, served from a CDN
    • Hydration attaches React to existing HTML with hydrateRoot
    • The client's first render must match the server HTML

    Likely follow-up: What causes a hydration mismatch?

  87. 87.What are portals in React, and when would you use one?mid

    createPortal(children, domNode) from react-dom renders children into a different DOM node, often one directly under document.body, while keeping them at the same place in the React tree.

    Typical uses are modals, tooltips, dropdown menus and toasts. A modal rendered deep inside a card with overflow: hidden, or inside a stacking context created by z-index or transform, gets clipped or layered incorrectly. A portal moves the DOM output out of that container, while you still write the modal right next to the button that opens it.

    Because a portal stays in the React tree:

    • it still receives context from its React ancestors,
    • events bubble through React ancestors, not DOM ancestors, so a click inside the modal also triggers onClick handlers on the components that render it, which can be surprising.

    For accessibility you still need focus management, closing on Escape and the right ARIA roles; the native dialog element is a good alternative for modals.

    import { createPortal } from 'react-dom';
    
    function Modal({ children, onClose }) {
      return createPortal(
        <div className="backdrop" onClick={onClose}>
          <div role="dialog" aria-modal="true" onClick={e => e.stopPropagation()}>
            {children}
          </div>
        </div>,
        document.body
      );
    }
    What interviewers listen for
    • createPortal renders into another DOM node
    • Escapes overflow clipping and z-index stacking contexts
    • Stays in the React tree, so context still works
    • Events bubble through React ancestors, not DOM ancestors
    • Still handle focus and keyboard accessibility
  88. 88.What was forwardRef used for, and what changed about passing refs to components in React 19?mid

    Before React 19, ref was a special attribute that React removed from props, so a function component couldn't receive it. If a parent wrote <TextInput ref={inputRef} /> to focus the inner input, TextInput had to be wrapped in forwardRef, which passed the ref as a second argument that the component forwarded onto a DOM element.

    In React 19, function components can read ref directly as a prop: function TextInput({ ref, ...props }). forwardRef still works but is no longer needed, and the React team has said it will be deprecated in a future version. For class components nothing changed: a ref on a class component still points to the instance and isn't passed as a prop.

    Related tools: useImperativeHandle lets a component expose a limited API, like { focus, scrollToTop }, instead of the whole DOM node, and in React 19 a ref callback can return a cleanup function.

    // React 18 and earlier
    const TextInput = forwardRef(function TextInput(props, ref) {
      return <input ref={ref} {...props} />;
    });
    
    // React 19
    function TextInput({ ref, ...props }) {
      return <input ref={ref} {...props} />;
    }
    
    // The parent is the same in both cases
    <TextInput ref={inputRef} />
    What interviewers listen for
    • ref was not a regular prop before React 19
    • forwardRef passed the ref as a second argument
    • React 19: function components receive ref as a prop
    • forwardRef still works but is slated for deprecation
    • useImperativeHandle exposes a custom handle
  89. 89.In development, your effect connects to a chat server twice when the component mounts. Is this a React bug, and what should you do?mid

    It's not a bug. In development, <StrictMode> mounts each component, runs its effects, then simulates an unmount by running the cleanups, and runs the effects again. It does this deliberately, to reveal effects that don't clean up after themselves. It never happens in production builds.

    So the double connection is telling you the effect is missing a correct cleanup. Make setup and cleanup symmetric: if the effect calls connection.connect(), the cleanup must call connection.disconnect(). Then connect, disconnect, connect looks the same to the user as a single connect. The same goes for subscriptions (unsubscribe), timers (clear) and fetches (abort, or ignore the stale result).

    Don't work around it with a "has already run" ref: that hides the symptom but keeps the bug, and React can legitimately remount components while preserving their state. For logic that must run exactly once when the app starts, put it at module level, outside any component.

    What interviewers listen for
    • StrictMode runs setup, cleanup, setup in development only
    • It exposes effects without a proper cleanup
    • Make the cleanup mirror the setup
    • Don't suppress it with a has-run ref
    • Put true one-time initialization at module level
  90. 90.A tooltip first appears in the wrong position and then jumps into place. Why does this happen, and how would you fix it?mid

    The tooltip probably measures itself in a useEffect and then stores its position in state. useEffect usually runs after the browser paints, so the user sees one frame of the tooltip in its initial position; then the effect measures, sets state, and a second paint moves it.

    The fix is useLayoutEffect. It runs after React has updated the DOM but before the browser paints. You read layout with getBoundingClientRect(), set state, and React processes that update before the screen is painted, so the user only ever sees the final position.

    Caveats:

    • It blocks painting, so keep the work small and use it only when you must measure before showing something.
    • It doesn't run during server rendering, so the server HTML won't include the measured values.
    • A positioning library like Floating UI already handles this, including flipping and collision detection.

    Default to useEffect, and switch to the layout version only for this kind of visual measurement.

    What interviewers listen for
    • useEffect usually runs after paint, causing a visible jump
    • useLayoutEffect runs after DOM updates, before paint
    • Measure and set state before the browser paints
    • It blocks painting, so use it sparingly
    • It does not run during server rendering
  91. 91.In class components, why did event handlers need this.handleClick = this.handleClick.bind(this), and why is that unnecessary in function components?mid

    In JavaScript, this is determined by how a function is called, not where it's defined. onClick={this.handleClick} passes the method as a bare function reference. When React later calls it, there's no object before the dot, so this is undefined (class bodies always run in strict mode) and this.setState throws a TypeError.

    The classic fixes:

    • Bind once in the constructor: this.handleClick = this.handleClick.bind(this).
    • Use a class field arrow function, handleClick = () => { ... }, because arrow functions capture this lexically from the instance.
    • Use an inline arrow, onClick={() => this.handleClick()}, which creates a new function on every render.

    Function components avoid the problem entirely, because there is no this. Handlers are ordinary functions declared inside the component that close over the current render's props and state. The trade-off shifted from binding bugs to being aware of stale closures, but the code is simpler.

    What interviewers listen for
    • this depends on how a function is called
    • Passing this.handleClick loses its receiver
    • Fix with bind, class field arrows or inline arrows
    • Function components have no this; handlers close over values
  92. 92.Why would you use a library like TanStack Query instead of fetching data in useEffect?mid

    Fetching in an effect looks simple, but a production-quality version needs a lot: loading and error states, ignoring stale responses when inputs change, caching so revisiting a screen is instant, deduplicating identical requests from several components, refetching stale data, retries, pagination, and refreshing data after a mutation.

    Server data isn't really client state; it's a cache of something the server owns. TanStack Query, SWR, RTK Query and Apollo (for GraphQL) are built around that idea. You call useQuery with a queryKey and a queryFn, and get back data, error and status flags. The key identifies the cache entry, so every component asking for the same key shares one request and one cached result. After a write, useMutation plus invalidateQueries refetches the affected data.

    Effects also create request waterfalls: a child can't start fetching until its parent has rendered. Frameworks avoid that by fetching in route loaders or Server Components, the other good alternative.

    function Todos({ filter }) {
      const { data, isPending, error } = useQuery({
        queryKey: ['todos', filter],
        queryFn: () => fetchTodos(filter),
      });
    
      if (isPending) return <Spinner />;
      if (error) return <ErrorMessage error={error} />;
      return <TodoList todos={data} />;
    }
    What interviewers listen for
    • Effect fetching needs race handling, caching, retries, dedupe
    • Server data is a cache, not client state
    • Query keys identify and share cached data
    • Mutations invalidate queries to refresh data
    • Effects cause waterfalls; loaders and Server Components avoid them
  93. 93.What is concurrent rendering in React, and which APIs let you take advantage of it?hard

    Before React 18, once React started rendering an update it had to finish synchronously, so a big render could block typing and clicks. Concurrent rendering makes rendering interruptible: React can start rendering an update, pause to handle something more urgent, and then resume, restart or throw away the unfinished work. Changes are committed only when a render completes, so users never see half-finished UI.

    It's opt-in per update rather than a global mode, and it requires createRoot. The APIs:

    • startTransition and useTransition mark a state update as non-urgent; urgent updates like typing interrupt it, and isPending lets you show feedback.
    • useDeferredValue renders with a lagging copy of a value, then catches up in the background.
    • <Suspense> with transitions keeps the previous UI on screen instead of showing a fallback while new content loads.

    Concurrency doesn't make rendering itself faster; it changes scheduling so the app stays responsive. It's also why render functions must be pure: a render may run more than once before it's committed.

    What interviewers listen for
    • Rendering becomes interruptible and resumable
    • Committed only when complete: no partial UI
    • Opt in per update with transitions and deferred values
    • Transitions keep old UI instead of Suspense fallbacks
    • Improves responsiveness, not raw render speed

    Likely follow-up: How is useDeferredValue different from debouncing?

  94. 94.What are the key differences between React and Angular?easy

    The main differences:

    • Scope: React is a UI library; routing, data fetching and forms come from the ecosystem or a framework like Next.js. Angular is a full, opinionated framework with a router, HTTP client, forms, dependency injection and a CLI built in.
    • Templates: React uses JSX, so markup lives in JavaScript and you loop with map. Angular uses HTML templates with its own syntax, such as @if, @for and property and event bindings.
    • Data flow and reactivity: React has one-way data flow and re-renders components when state changes. Angular also supports two-way binding with [(ngModel)] and updates the view through change detection, increasingly driven by signals.
    • Language: React works with JavaScript or TypeScript; Angular is TypeScript-first.
    • Architecture: Angular has services and dependency injection built in; React shares logic through hooks and context.

    Neither is better overall. React offers flexibility and a huge ecosystem, but teams must choose libraries and conventions. Angular offers consistency out of the box, which suits large teams, at the cost of a steeper learning curve.

    What interviewers listen for
    • React is a UI library; Angular is a full framework
    • JSX versus HTML templates with Angular syntax
    • One-way data flow versus two-way binding and change detection
    • Angular is TypeScript-first with built-in dependency injection
    • Flexibility versus built-in conventions
  95. 95.What is React Fiber, and what problem did it solve?hard

    Fiber is the reconciliation engine React has used since React 16, a rewrite of the older "stack" reconciler. The old reconciler walked the component tree recursively and synchronously, so a large update couldn't be paused and could block the main thread.

    Fiber represents each component instance, DOM element or fragment as a fiber: a plain object holding its type, props, state and hooks, pending work, and pointers to its child, sibling and parent. Because traversal is a loop over these linked nodes rather than deep recursion, React can split rendering into small units of work, pause between them to yield to the browser, prioritize urgent updates, and resume or discard work.

    React keeps two trees: the current tree that's on screen and a work-in-progress tree being built, swapped on commit. The render phase is interruptible; the commit phase is synchronous, so the DOM never shows a partial update.

    Fiber is the foundation for error boundaries, Suspense, transitions and concurrent rendering. It's an internal implementation detail you don't use directly.

    What interviewers listen for
    • The reconciler rewrite used since React 16
    • Each node is a fiber: a unit of work with links
    • Rendering can pause, resume, prioritize or be discarded
    • Current and work-in-progress trees; commit is synchronous
    • Foundation for Suspense and concurrent features
  96. 96.How would you render a list of 10,000 rows without the page becoming sluggish?hard

    The main technique is virtualization, also called windowing: render only the rows visible in the viewport, plus a small overscan buffer, instead of all 10,000. The scroll container keeps the full list height through a spacer or padding, and rows are positioned by index, so as you scroll the same few dozen DOM nodes are reused with new data. Libraries include TanStack Virtual, react-window and react-virtuoso, which handles variable heights well.

    Supporting measures:

    • Stable keys and memo on the row component, so scrolling doesn't re-render every visible row.
    • Keep filtering and sorting cheap with useMemo, and use useDeferredValue for the search query.
    • Consider pagination or infinite loading if users don't need everything at once.

    Trade-offs worth mentioning: variable row heights need measuring, the browser's find-in-page can't see rows that aren't rendered, and screen readers need extra ARIA hints about the total size. For moderately long pages, CSS content-visibility: auto is a lighter alternative.

    What interviewers listen for
    • Virtualize: render only visible rows plus overscan
    • A spacer keeps full scroll height; DOM nodes are reused
    • TanStack Virtual, react-window or react-virtuoso
    • Memoize rows and use stable keys
    • Trade-offs: variable heights, find-in-page, accessibility
  97. 97.What are the render and commit phases, and why must components be pure during rendering?hard

    React splits an update into a render phase, where it calls your components to compute JSX, and a commit phase, where it applies the changes to the DOM and then runs effects. Rendering is treated as a pure calculation: given the same props, state and context, a component should return the same JSX and change nothing outside itself.

    React relies on this because a render isn't guaranteed to be committed. With concurrent features, React may start, pause, discard or repeat a render, and StrictMode renders twice in development precisely to expose impure components.

    Side effects that don't belong in render:

    • mutating props, state, or variables defined outside the component,
    • touching the DOM, localStorage or a global cache,
    • starting fetches, subscriptions or timers,
    • reading or writing ref.current, except for lazy initialization.

    Mutating an object you created during the same render is fine; that's local mutation. Side effects belong in event handlers, or in effects when they synchronize with an external system. Purity is also what lets the React Compiler memoize safely.

    What interviewers listen for
    • Render computes JSX; commit applies DOM changes and runs effects
    • Same inputs must produce the same output
    • Renders may be interrupted, discarded or repeated
    • No external mutation, I/O or subscriptions during render
    • Local mutation of freshly created objects is fine
  98. 98.How do you reset a component's internal state when a prop such as userId changes?mid

    React preserves a component's state as long as the same component type renders at the same position in the tree. So when <Profile userId={id} /> receives a new id, its internal state, such as a half-typed comment, is kept, which is often a bug.

    The idiomatic fix is a key: <Profile key={userId} userId={userId} />. When the key changes, React treats it as a different component: it unmounts the old instance, discarding its state and all of its children's state, and mounts a fresh one. No effect is needed.

    Avoid resetting state in a useEffect that watches the prop: the component first renders with the stale state and then again after the reset, and you have to remember to reset every piece of state. If only part of the state should reset, it's usually cleaner to restructure, for example by storing a selected id instead of a copied object, so the value is derived during render.

    What interviewers listen for
    • State is tied to component type and position in the tree
    • Changing the key remounts with fresh state
    • Resetting in an effect causes an extra, stale render
    • Prefer storing ids and deriving values during render
  99. 99.What does the 'use client' directive do? Does it mean the component only renders in the browser?hard

    No, and that's a common misconception. In an app using React Server Components, like the Next.js App Router, components are Server Components by default. 'use client' at the top of a file marks a boundary: that module, and every module it imports, becomes part of the client bundle, so its components can use state, effects, event handlers and browser APIs.

    Client Components are still pre-rendered to HTML on the server and then hydrated in the browser. "Client" means the code also ships to and runs in the browser, not that the server skips it. That's why browser-only APIs like window still belong in effects or event handlers.

    Other details:

    • It must come first in the file, before any imports.
    • It follows the import graph, not the render tree: a Server Component passed as children to a Client Component stays on the server.
    • Props passed from Server to Client Components must be serializable, so no regular functions.
    • Place it as low as possible, on interactive leaves, to keep the bundle small.
    What interviewers listen for
    • Marks a module and its imports as client code
    • Client Components are still server-rendered, then hydrated
    • The boundary follows imports, not the render tree
    • Props crossing the boundary must be serializable
    • Push it down to interactive leaf components
  100. 100.How does React protect against XSS, and what are the risks of dangerouslySetInnerHTML?mid

    By default React escapes strings rendered in JSX: <div>{comment}</div> inserts the value as text, so an injected <script> tag or onerror attribute shows up as literal characters instead of executing. That removes the most common XSS vector.

    dangerouslySetInnerHTML={{ __html: html }} bypasses this and sets innerHTML directly. The awkward name and object shape are deliberate, so it stands out in code review. If the HTML comes from users or any untrusted source, sanitize it first with a library like DOMPurify, or render Markdown to React elements instead of raw HTML.

    Other holes that escaping doesn't cover:

    • User-supplied URLs in href or src: a javascript: URL can run code when clicked, so allow only safe protocols like https:.
    • Spreading untrusted objects as props, which can inject dangerouslySetInnerHTML or event handlers.
    • Serializing state into an inline script during SSR without escaping it.

    Add defense in depth with a Content Security Policy.

    What interviewers listen for
    • JSX escapes rendered strings by default
    • dangerouslySetInnerHTML bypasses escaping; sanitize with DOMPurify
    • Validate user URLs; reject javascript: protocols
    • Don't spread untrusted objects as props
    • Add a Content Security Policy
  101. 101.In React Testing Library, what is the difference between getBy, queryBy and findBy queries, and which query types should you prefer?mid

    They differ in what happens when the element isn't there:

    • getBy... returns the element or throws if there's no match, or more than one. Use it for elements that should be present right now.
    • queryBy... returns null when nothing matches, which makes it the one for asserting absence, e.g. expect(screen.queryByText('Error')).not.toBeInTheDocument().
    • findBy... returns a promise that retries until the element appears or a timeout (1 second by default) passes. Use it with await for async UI, like data that arrives after a fetch.

    Each has an All variant (getAllBy, queryAllBy, findAllBy) for multiple matches.

    For the query type, follow the library's priority: ByRole (with a name option) first, then ByLabelText, ByPlaceholderText and ByText, and ByTestId only as a last resort, since users can't see test ids. Prefer screen over destructuring from render, and user-event over fireEvent for realistic interactions.

    test('shows the user after loading', async () => {
      render(<UserCard id="1" />);
    
      expect(screen.getByRole('progressbar')).toBeInTheDocument();
      expect(await screen.findByRole('heading', { name: 'Ada' })).toBeInTheDocument();
      expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
    });
    What interviewers listen for
    • getBy throws when missing: for elements that should exist
    • queryBy returns null: for asserting absence
    • findBy returns a retrying promise: for async UI
    • Prefer ByRole; use ByTestId as a last resort
    • All variants handle multiple matches
  102. 102.How do you make a React application accessible?mid

    Most accessibility in React is simply good HTML, plus care in the areas a single-page app makes easy to get wrong:

    • Semantic elements first: button for actions, a for navigation, label for inputs, headings in order. A clickable div needs a role, tabIndex and keyboard handlers that a button gives you for free.
    • JSX naming: htmlFor instead of for, while aria-* attributes keep their HTML names, e.g. aria-expanded={open}.
    • Unique ids for aria-labelledby and aria-describedby from useId, which stays consistent between server and client, instead of hard-coded ids that clash when a component renders twice.
    • Focus management: move focus to the main heading after client-side navigation, trap focus in modals and restore it on close, using refs.
    • Announcements: use an aria-live region for async updates like "Saved".
    • Testing: keyboard-only and screen reader checks, eslint-plugin-jsx-a11y, axe, and role-based queries in Testing Library.

    For complex widgets like comboboxes, follow the WAI-ARIA patterns or use accessible headless libraries such as React Aria or Radix.

    What interviewers listen for
    • Semantic HTML first: real buttons, links and labels
    • htmlFor, aria-* attributes, and useId for unique ids
    • Manage focus on navigation and in modals
    • Announce async changes with live regions
    • Test with keyboard, screen readers, axe and jsx-a11y
  103. 103.What causes hydration mismatch errors, and how do you fix them?hard

    During hydration, React expects the browser's first render to produce exactly the HTML the server sent. When it doesn't, React reports a hydration error. It can recover from some mismatches by re-rendering the affected part on the client, which throws away that server HTML and costs performance, and attribute differences aren't guaranteed to be patched, so treat every mismatch as a bug.

    Common causes:

    • Values that differ between server and client: Date.now(), Math.random(), locale- or timezone-dependent formatting.
    • Browser-only checks during render, like typeof window !== 'undefined', localStorage or window.innerWidth.
    • Invalid HTML nesting, such as a div inside a p, which the browser's parser rearranges.
    • Browser extensions modifying the page before React hydrates.

    Fixes: make the first client render match the server, then switch to client-only values in an effect, for example with a mounted flag set in useEffect. Generate ids with useId rather than counters or random values, and compute timestamps once on the server and pass them down. For unavoidable text differences, suppressHydrationWarning silences the warning on a single element.

    What interviewers listen for
    • The client's first render must match the server HTML
    • Causes: time, randomness, locale, browser-only APIs in render
    • Invalid HTML nesting and extensions also cause mismatches
    • Render client-only values after mount, in an effect
    • useId for ids; suppressHydrationWarning as a last resort
  104. 104.How does useActionState work? Walk through a form that displays a validation error returned by its action.hard

    useActionState(action, initialState) wraps an action and keeps track of its result. It returns [state, formAction, isPending]: the latest value returned by the action (starting as initialState), a wrapped action to pass to a form's action prop or a button's formAction, and a pending flag.

    When the form is submitted, React calls your action with the previous state first and the FormData second; that extra first argument changes the action's signature. Whatever it returns, or resolves to, becomes the new state, so a validation error or success message is simply returned, as in subscribe above. The action runs inside a transition, so isPending stays true until it finishes, and repeated submissions are processed in order, since each receives the previous state.

    Worth knowing: uncontrolled fields are reset after the action completes, even when it returned an error, so if you want to keep what the user typed, return it in state and render it back as the input's defaultValue. With a framework that supports Server Functions, the action can be a 'use server' function.

    async function subscribe(prevState, formData) {
      const email = formData.get('email');
      if (!email.includes('@')) return { error: 'Enter a valid email' };
      await saveSubscriber(email);
      return { error: null };
    }
    function NewsletterForm() {
      const [state, formAction, isPending] = useActionState(subscribe, { error: null });
      return <form action={formAction}>
        <input name="email" />
        <button disabled={isPending}>Subscribe</button>
        {state.error && <p role="alert">{state.error}</p>}
      </form>;
    }
    What interviewers listen for
    • Returns [state, formAction, isPending]
    • The action receives previous state, then FormData
    • Its return value becomes the next state
    • Runs in a transition; isPending tracks it
    • Uncontrolled fields reset after the action completes
  105. 105.What is useOptimistic, and how would you use it to make a "like" button feel instant?hard

    useOptimistic(state, updateFn) shows a temporary, optimistic version of some state while an async action is in progress. It returns [optimisticState, addOptimistic]. Calling addOptimistic(value) makes React compute updateFn(currentState, value) and render that immediately. When the action finishes, the optimistic value is dropped and the component shows whatever the real state is by then.

    For a like button: inside the action, call addOptimisticLike(1) first so the count bumps instantly, then await the server call. On success, the real likes has been updated, so dropping the optimistic value changes nothing visible. On failure, the real state never changed, so the UI reverts automatically; you should also tell the user it failed.

    Rules: call addOptimistic inside an Action or startTransition, such as a function passed to a form's action prop; React warns if you call it outside one. Keep updateFn pure. It fits likes, toggles and appending a pending message to a chat list.

    function LikeButton({ likes, likeAction }) {
      const [optimisticLikes, addOptimisticLike] = useOptimistic(
        likes,
        (current, amount) => current + amount
      );
    
      async function handleLike() {
        addOptimisticLike(1); // shown immediately
        await likeAction();   // server call; the parent then passes the new likes
      }
    
      return <form action={handleLike}><button>Like ({optimisticLikes})</button></form>;
    }
    What interviewers listen for
    • Shows temporary state while an action is pending
    • Returns [optimisticState, addOptimistic]
    • updateFn(current, value) computes the optimistic value
    • Falls back to the real state when the action settles
    • Call it inside an Action or startTransition
  106. 106.What is React 19's use API, and how is it different from other hooks?hard

    use(resource) reads the value of a promise or a context during render.

    With a promise, use suspends the component until it resolves: the nearest <Suspense> boundary shows its fallback, and once the promise resolves React renders again with the value. If the promise rejects, the nearest error boundary shows its fallback; alternatively, attach .catch() to the promise to provide a fallback value.

    With a context, use(ThemeContext) works like useContext.

    What's different: unlike hooks, use can be called inside conditions and loops, though still only inside components or hooks, and not inside a try/catch block.

    The important caveat: don't create the promise during render in a Client Component, as in use(fetch(url)). Every render would create a new promise, so the component would keep suspending. The promise should be created outside render and cached: typically a Server Component starts the fetch and passes the promise to a Client Component, or a Suspense-enabled library or framework provides it.

    What interviewers listen for
    • Reads a promise or a context during render
    • Suspends until resolved; rejections reach error boundaries
    • May be called conditionally, unlike other hooks
    • Cannot be called inside try/catch
    • The promise must be cached, not created during render
  107. 107.What is the compound components pattern, and how would you implement one?hard

    Compound components are a set of components that work together and share implicit state, much like the native select and option elements. Instead of one component with a huge configuration prop, you expose composable parts such as Tabs, Tab and TabPanel, often attached as Tabs.Tab. The parent owns the state (the active tab) and shares it with its parts through context, so consumers control the markup, order and styling while the logic stays inside.

    Benefits: a flexible, readable API, no explosion of props like renderTabLabel or tabClassName, and consumers can put their own elements between the parts.

    Implementation tips:

    • Throw a helpful error when a part is used outside its parent.
    • Memoize the context value.
    • Support both controlled (value plus onChange) and uncontrolled (defaultValue) usage.

    Older implementations used React.Children.map and cloneElement to inject props, but that breaks as soon as a child is wrapped in another element, so context is preferred. Headless libraries like Radix UI and Headless UI use this pattern heavily.

    const TabsContext = createContext(null);
    function Tabs({ defaultValue, children }) {
      const [active, setActive] = useState(defaultValue);
      const value = useMemo(() => ({ active, setActive }), [active]);
      return <TabsContext value={value}>{children}</TabsContext>;
    }
    function Tab({ value, children }) {
      const { active, setActive } = useContext(TabsContext);
      return <button role="tab" aria-selected={active === value} onClick={() => setActive(value)}>{children}</button>;
    }
    function TabPanel({ value, children }) {
      const { active } = useContext(TabsContext);
      return active === value ? <div role="tabpanel">{children}</div> : null;
    }
    What interviewers listen for
    • Related components that share implicit state
    • The parent owns state and shares it through context
    • Consumers control structure and markup
    • Avoids giant configuration props
    • Context is more robust than cloneElement
  108. 108.What is the React Compiler, and does it mean you no longer need useMemo and useCallback?hard

    The React Compiler is a build-time tool, typically run as a Babel plugin, that analyzes components and hooks and inserts memoization automatically. It caches computed values, callbacks and JSX at a fine-grained level, so work is skipped when inputs haven't changed, much like hand-written memo, useMemo and useCallback, but applied consistently.

    It depends on your code following the Rules of React: pure rendering, no mutation of props or state, and hooks called unconditionally. When it can't be sure a component is safe to optimize, it leaves that component as is rather than risk breaking it, and its ESLint rules report code that breaks the rules.

    So with the compiler enabled, you rarely need to add manual memoization in new code. But:

    • existing useMemo and useCallback calls remain valid, and removing them can change behavior when they stabilize effect dependencies,
    • manual memoization is still an escape hatch for precise control,
    • you still need to understand referential equality to debug behavior.

    It's opt-in, and it won't fix slow algorithms or poor state architecture.

    What interviewers listen for
    • A build-time tool that auto-memoizes components and hooks
    • Requires code that follows the Rules of React
    • Skips components it cannot safely optimize
    • Reduces the need for manual memo, useMemo, useCallback
    • Manual memoization remains an escape hatch
  109. 109.How do you subscribe a component to an external data source, like the browser online status or a third-party store, correctly in React?hard

    Use useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?):

    • subscribe(callback) registers a listener with the source and returns an unsubscribe function.
    • getSnapshot() returns the current value. React calls it during render and re-renders when it returns a different value, compared with Object.is.
    • getServerSnapshot() provides the value used during server rendering and hydration.

    Why not useState plus a subscription in useEffect? With concurrent rendering, React may render parts of the tree at different times; if the store changes in between, components can show different values for the same data, which is called tearing. useSyncExternalStore guarantees a consistent snapshot, and it reads the value during render, so there's no first render with a stale value.

    Gotchas: getSnapshot must return a cached value, not a new object on each call, or React re-renders endlessly; and subscribe should be defined outside the component, or memoized, so React doesn't resubscribe on every render. Libraries like Redux and Zustand use this hook under the hood.

    function subscribe(callback) {
      window.addEventListener('online', callback);
      window.addEventListener('offline', callback);
      return () => {
        window.removeEventListener('online', callback);
        window.removeEventListener('offline', callback);
      };
    }
    
    function useOnlineStatus() {
      return useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
    }
    What interviewers listen for
    • useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
    • Prevents tearing under concurrent rendering
    • Reads the snapshot during render: no stale first frame
    • getSnapshot must return a cached, immutable value
    • Define subscribe outside the component
  110. 110.What is reconciliation in React, and why do keys matter?easy

    Reconciliation is how React works out what to change in the DOM after a re-render. Rendering produces a new tree of elements; React compares it with the previous tree and applies the minimal set of DOM operations in the commit phase.

    A general tree diff is far too expensive, so React uses two heuristics that make it linear:

    • Different element type at the same position (div to span, or ProfileA to ProfileB): React tears down the old subtree, including its state, and mounts a new one.
    • Same type: React keeps the DOM node or component instance, updates the changed props, and recurses into the children.

    For lists, React matches children by key. Stable, unique keys let it move, insert and delete items correctly. Index keys break down when items are reordered, inserted or removed, because state and DOM values stay attached to positions rather than to items.

    What interviewers listen for
    • Diffs the new element tree against the previous one and commits minimal DOM changes
    • Different type at the same position remounts the subtree and loses state
    • Same type keeps the instance and updates props
    • Keys identify list children; use stable ids, not indexes, for dynamic lists

    Likely follow-up: Why is using the array index as a key a problem? · How can you deliberately reset a component's state?

  111. 111.What causes a React component to re-render, and what happens during a render?easy

    A component re-renders when its own state changes, when its parent re-renders, or when a context it reads changes. "Props changed" isn't a separate trigger: props only change because the parent rendered.

    An update has two phases:

    • Render: React calls your component functions to compute the new JSX. This must be pure: no DOM mutation, subscriptions or other side effects.
    • Commit: React applies the differences to the DOM, runs layout effects, lets the browser paint, and then runs passive effects (useEffect).

    A re-render doesn't necessarily change the DOM: if the output is the same, the commit has nothing to do. Setting state to the same value (by Object.is) lets React bail out. To avoid re-rendering expensive children you can wrap them in memo, move state closer to where it's used, or pass children in as props.

    What interviewers listen for
    • Triggers: own state update, parent re-render, context change
    • Render phase is pure computation; commit phase touches the DOM
    • A re-render doesn't imply DOM changes
    • Same-value state updates bail out (Object.is)

    Likely follow-up: Does a child re-render if its props didn't change? · How does passing children from a parent help avoid re-renders?

  112. 112.What is the difference between useEffect and useLayoutEffect, and when would you use each?mid

    Both run after React has updated the DOM; the difference is timing relative to the browser's paint.

    • useEffect normally runs after the browser has painted, so it doesn't block the screen update. It's the default for side effects: fetching, subscriptions, timers, analytics, syncing with non-React widgets. (For effects caused by a discrete interaction such as a click, React may flush them before paint.)
    • useLayoutEffect runs synchronously after DOM mutations but before paint. Use it when you must read layout, e.g. getBoundingClientRect, and immediately re-render: positioning a tooltip or popover so the user never sees it flicker in the wrong place.

    Because layout effects block painting, overusing them hurts performance, so default to useEffect and reach for the layout version only for visual measurements. useLayoutEffect also doesn't run during server rendering, so layout-dependent UI needs a sensible initial render.

    What interviewers listen for
    • Both run in the commit phase after DOM mutations
    • useLayoutEffect runs before paint and blocks it
    • Use the layout version for measuring layout and avoiding flicker
    • Default to useEffect; layout effects cost performance

    Likely follow-up: In what order do parent and child effects run?

  113. 113.When does a useEffect cleanup function run, and how do you avoid race conditions when fetching data in an effect?mid

    The cleanup runs before the effect runs again with changed dependencies, and when the component unmounts. In development, StrictMode also runs one extra setup-then-cleanup cycle on mount to prove the cleanup is correct.

    A fetch race happens when a dependency changes quickly: request A starts, then request B, but A resolves last and overwrites B's result. Two standard fixes:

    • An ignore flag: declare let ignore = false in the effect, set it to true in the cleanup, and only call setState when !ignore.
    • An AbortController: pass its signal to fetch and call controller.abort() in the cleanup, which also cancels the network request.

    In production code I'd usually use a data-fetching library like TanStack Query or SWR, or the framework's loaders, which handle caching, deduplication and races for you.

    What interviewers listen for
    • Cleanup runs before the next effect run and on unmount
    • StrictMode adds an extra setup/cleanup cycle in development
    • Late responses can overwrite fresh ones
    • Fix with an ignore flag or AbortController in cleanup
    • Prefer a data-fetching library for real apps

    Likely follow-up: Why does the effect run twice in development?

  114. 114.Explain memo, useMemo and useCallback. When are they worth using?mid

    memo wraps a component so React skips re-rendering it when every prop is equal (Object.is) to last time. useMemo caches a computed value between renders until its dependencies change. useCallback caches a function's identity; it's useMemo for functions.

    They work together: memo only helps if the props are stable, so object and function props passed to a memoized child often need useMemo/useCallback. Stable identities also matter for effect dependency arrays.

    I use them for measured problems: a genuinely expensive calculation, a large subtree re-rendering on every keystroke, or a dependency that would otherwise re-trigger an effect. They aren't free: they add comparisons, memory and complexity, and wrong dependencies cause stale values. Often restructuring, like moving state down or passing children, fixes re-renders more cleanly. The React Compiler can also apply this memoization automatically.

    What interviewers listen for
    • memo skips a re-render when props are shallowly equal
    • useMemo caches values; useCallback caches function identity
    • memo needs stable props to be effective
    • Use them for measured problems, not by default
    • Restructuring state or the React Compiler can remove the need

    Likely follow-up: Why does passing an inline object to a memoized child defeat memo?

  115. 115.What is the difference between controlled and uncontrolled components?easy

    A controlled input gets its value from React state through value plus onChange, so React is the single source of truth. You can validate or format on every keystroke, derive other UI from the value, and reset the field by setting state.

    An uncontrolled input keeps its value in the DOM. You give it a defaultValue and read the value when you need it, through a ref or from FormData on submit.

    Controlled inputs suit instant validation, dependent fields and complex forms, but each keystroke re-renders the owning component. Uncontrolled inputs are simpler and cheaper, and React 19 form actions lean on them: the action receives FormData, and uncontrolled fields reset after a successful action. Form libraries like React Hook Form are largely uncontrolled for performance.

    Don't switch a field between the two: going from value={undefined} to a string triggers a warning.

    What interviewers listen for
    • Controlled: value lives in React state (value + onChange)
    • Uncontrolled: the DOM holds the value (defaultValue + ref or FormData)
    • Controlled enables instant validation but re-renders on each keystroke
    • Don't switch between controlled and uncontrolled
  116. 116.What is a custom hook, and what makes a good one?easy

    A custom hook is a function whose name starts with use and that calls other hooks. It lets you extract stateful logic, like tracking online status, debouncing a value or managing a WebSocket, and reuse it across components.

    Custom hooks share logic, not state: each component that calls useOnlineStatus() gets its own independent state and effects. To share the same state you lift it up or use context or a store.

    A good custom hook:

    • has one focused purpose and a descriptive name, like useDebouncedValue,
    • follows the Rules of Hooks: called unconditionally at the top level,
    • returns a small, stable API, memoizing returned functions if callers may use them as dependencies,
    • cleans up everything it sets up.

    I avoid hooks that just wrap lifecycles, like useMount, because they hide what the effect is actually synchronizing with.

    What interviewers listen for
    • A function starting with use that calls other hooks
    • Shares logic, not state: every call is independent
    • Must follow the Rules of Hooks
    • Focused purpose, stable return values, proper cleanup

    Likely follow-up: How would you test a custom hook?

  117. 117.What are the performance pitfalls of React Context, and how do you mitigate them?hard

    Every component that reads a context re-renders when the provider's value changes by Object.is, and memo on the consumer doesn't prevent it. Two things commonly go wrong:

    • Unstable values: value={{ user, setUser }} creates a new object on every render of the provider, so all consumers re-render even when nothing changed. Memoize the value with useMemo, and callbacks with useCallback.
    • Too much in one context: if one context holds the user, theme and cart, every cart update re-renders every theme consumer.

    Mitigations: split contexts by how often they change, for example separate state and dispatch contexts; keep providers as low in the tree as possible; pass stable values. For high-frequency, widely shared state, use a store with selector-based subscriptions (Zustand, Redux, Jotai, or useSyncExternalStore) so a component only re-renders when the slice it selects changes. Context shines for low-frequency values like theme, locale and the current user.

    What interviewers listen for
    • All consumers re-render when the value changes; memo does not block it
    • Memoize the provider value
    • Split contexts by update frequency; separate state and dispatch
    • Use selector-based stores for high-frequency shared state

    Likely follow-up: How does useSyncExternalStore let a store avoid unnecessary renders?

  118. 118.What is an error boundary, and what can it not catch?mid

    An error boundary is a component that catches errors thrown while rendering the tree below it and shows a fallback UI instead. Without one, an uncaught rendering error unmounts the entire React root.

    There's still no hook for this: you write a class component with static getDerivedStateFromError to render the fallback and componentDidCatch to log, or use the react-error-boundary package, which adds reset support.

    Boundaries don't catch errors in event handlers, in asynchronous code like timers or promise callbacks, or in the boundary itself. For handlers you use try/catch; if you want the boundary to show the failure, store the error in state and throw it during render.

    In practice I place a boundary at route level and around independent widgets, so one broken widget doesn't take down the page, and I report caught errors to a monitoring service.

    What interviewers listen for
    • Catches errors during rendering of its subtree and shows a fallback
    • Class component with getDerivedStateFromError / componentDidCatch, or react-error-boundary
    • Doesn't catch event handler or async errors
    • Place at route and widget level; report to monitoring

    Likely follow-up: How would you let the user retry after an error?

  119. 119.How do Suspense and lazy work together, and where would you place Suspense boundaries?mid

    lazy code-splits a component: const Editor = lazy(() => import('./Editor')) loads the module the first time Editor renders. The module needs a default export, and lazy should be called at module top level.

    <Suspense fallback={...}> shows its fallback while anything inside it is suspended: a lazy component still loading, a promise read with use(), or data from a Suspense-enabled framework or library. It does not detect data fetched in useEffect.

    Boundary placement is a UX decision. One boundary around the page means one spinner until everything is ready; nested boundaries reveal content progressively, for example the header first and then the comments. If already-visible content suspends again, it's replaced by the fallback, unless the update is a transition, in which case React keeps showing the old UI. On the server, Suspense boundaries enable streaming SSR: the shell streams first and each boundary's HTML follows when ready.

    What interviewers listen for
    • lazy + dynamic import() for code splitting; default export; declared at top level
    • Suspense shows a fallback while its children suspend
    • Works with lazy, use() and Suspense-enabled data sources, not useEffect fetching
    • Boundary placement controls the loading sequence
    • Transitions keep old UI visible; boundaries enable streaming SSR
  120. 120.What does StrictMode do, and why do components and effects run twice in development?mid

    <StrictMode> is a development-only tool for finding bugs early; it has no effect in production builds. In development it:

    • Renders components twice, and calls state initializers and updater functions twice, to expose impure rendering such as mutating props or pushing into an outer array.
    • Runs effects one extra time on mount (setup, cleanup, setup) to check that every effect cleans up after itself. A chat connection without a disconnect will visibly connect twice.
    • Runs ref callbacks one extra time on mount, for the same reason.
    • Warns about deprecated APIs.

    The extra cycle simulates React unmounting and remounting a component with its state preserved, something features that hide and restore UI rely on. The right response is to make rendering pure and effects symmetric, never to remove StrictMode. For fetch-on-mount, the duplicate is harmless if you ignore or abort the stale request.

    What interviewers listen for
    • Development only; no effect in production
    • Double-invokes rendering to catch impure components
    • Extra setup/cleanup cycle for effects (and ref callbacks) on mount
    • Fix the code (pure render, symmetric cleanup), not StrictMode
  121. 121.What are React Server Components, and how do they differ from Client Components?hard

    Server Components run only on the server, at request time or build time, and their code never ships to the browser. They can be async, read databases or files directly, and keep secrets and heavy dependencies off the client. Their rendered output is serialized and streamed to the browser.

    Client Components are the components we've always written: they're pre-rendered to HTML too, but they also hydrate and run in the browser, so they can use state, effects, event handlers and browser APIs. You mark the boundary with 'use client' at the top of a file; that module and everything it imports become client code.

    Rules of thumb: keep data fetching and static content in Server Components, push 'use client' down to the interactive leaves, and pass Server Components into Client Components as children. Props crossing the boundary must be serializable. 'use server' is different: it marks Server Functions that the client can call, such as form actions. RSC need framework support, like the Next.js App Router.

    What interviewers listen for
    • Run only on the server; no client JS; can be async and access data directly
    • No state, effects or event handlers in Server Components
    • 'use client' marks the boundary; imported modules become client code
    • Props across the boundary must be serializable
    • 'use server' marks Server Functions, not Server Components

    Likely follow-up: How can a Client Component render a Server Component?

  122. 122.Compare useTransition and useDeferredValue. How are they different from debouncing?hard

    Both mark some rendering work as non-urgent, so urgent updates like typing and clicking stay responsive. A transition render can be interrupted and restarted when newer input arrives.

    • useTransition returns [isPending, startTransition]. You wrap a state update you own: startTransition(() => setTab('posts')). It suits tab switches and navigation, gives you isPending for a subtle loading indicator, and keeps already-visible content on screen instead of showing a Suspense fallback. In React 19 you can pass it an async function, which React calls an Action.
    • useDeferredValue(value) defers a value: the component first re-renders with the old value, then renders the new value in the background. Use it when you don't control the update, such as a prop, or to keep an input urgent while a heavy list reads the deferred query.

    Unlike debouncing, there's no fixed delay: on a fast device the deferred render happens almost immediately, and slow renders get interrupted rather than queued.

    What interviewers listen for
    • Both mark work as non-urgent and interruptible
    • useTransition wraps a state update you own and gives isPending
    • useDeferredValue lags a value you receive
    • Don't put a controlled input's own state in a transition
    • No fixed delay, unlike debounce or throttle
  123. 123.How do you decide where state should live in a React app, and when do you reach for a state management library?mid

    I start by classifying the state:

    • Server state (data from APIs): a cache library such as TanStack Query or SWR, or the framework's loaders. They handle caching, deduplication, refetching and invalidation, which hand-rolled global stores do badly.
    • URL state (filters, pagination, the selected tab): keep it in the URL so it's shareable and survives reloads.
    • Form state: local state, uncontrolled inputs with actions, or a form library.
    • Local UI state: useState or useReducer in the component that owns it, lifted only as far as needed.
    • Global client state (theme, current user, a cart): Context for low-frequency values; a store like Zustand, Redux Toolkit or Jotai when many components read and update it often, since stores offer selectors and devtools.

    Once server state is in a cache, the remaining global state is usually small. I'd choose Redux Toolkit for large teams that want strict conventions and devtools, and Zustand for lightweight shared state.

    What interviewers listen for
    • Separate server state from client state
    • Server state belongs in a cache library (TanStack Query, SWR)
    • Put shareable state in the URL
    • Keep state local; lift only as needed
    • Context for low-frequency globals; selector-based stores for frequent updates
  124. 124.How do you approach testing React components?easy

    I test behavior the way a user experiences it, not implementation details. With React Testing Library I render the component, find elements the way a user would (getByRole, getByLabelText), interact through user-event, and assert on what's visible.

    I avoid asserting on internal state, hook call counts or component instances, because those tests break on refactors that don't change behavior, while missing real bugs.

    The mix I aim for:

    • Unit tests for pure logic and custom hooks.
    • Component and integration tests for most UI, mocking the network at the boundary, e.g. with MSW, instead of mocking every module.
    • A few end-to-end tests with Playwright or Cypress for critical flows like sign-in and checkout.

    Role-based queries double as a light accessibility check. For async UI I use findBy* queries or waitFor instead of arbitrary timeouts.

    What interviewers listen for
    • Test behavior through the UI, not implementation details
    • Query by role or label like a user; use user-event for interactions
    • Mock the network boundary (MSW), not every module
    • Unit for logic, integration for most UI, a few E2E for critical flows
  125. 125.What are refs used for, and how do you pass a ref to your own component in React 19?easy

    A ref is a mutable box, { current }, that persists across renders; changing it does not trigger a re-render. Two main uses:

    • DOM access: focusing an input, measuring an element, scrolling into view, or integrating a non-React library.
    • Values that don't affect rendering: interval ids, an AbortController, the previous value of a prop.

    Don't read or write ref.current during rendering (apart from lazy initialization), because that makes render impure; use refs in effects and event handlers.

    In React 19, function components receive ref as a regular prop, so function Input({ ref, ...props }) { return <input ref={ref} {...props} /> } just works and forwardRef is no longer needed. Ref callbacks can now return a cleanup function. To expose a limited API instead of the raw DOM node, use useImperativeHandle.

    What interviewers listen for
    • Mutable current that survives renders without causing re-renders
    • Used for DOM access and non-rendering values
    • Don't read or write refs during render
    • React 19: ref is a normal prop; forwardRef not needed
    • useImperativeHandle exposes a custom handle
  126. 126.When should you NOT use useEffect? Give examples.mid

    Effects are for synchronizing with external systems: the network, subscriptions, timers, DOM outside React. Many effects in real code don't do that; they just add an extra render and bugs.

    Common cases to avoid:

    • Derived data: instead of an effect that calls setFullName(first + ' ' + last), compute fullName during render, with useMemo if it's expensive.
    • Responding to events: sending a POST when the user clicks Buy belongs in the click handler, not in an effect watching a submitted flag.
    • Resetting state when a prop changes: pass a key so React remounts the component.
    • Notifying a parent: call the parent's callback in the same handler that updates state.
    • Chains of effects that set state to trigger other effects: compute the next state in one place.

    Rule of thumb: if code runs because the component was displayed, it's an effect; if it runs because the user did something, it belongs in an event handler.

    What interviewers listen for
    • Effects are for syncing with external systems
    • Compute derived values during render
    • User-triggered logic belongs in event handlers
    • Reset state with a key instead of an effect
    • Avoid chains of effects that set state
  127. 127.How do Actions and form handling work in React 19?hard

    In React 19, an Action is a function, usually async, that runs inside a transition. You can pass one directly to <form action={fn}>: React calls it with the form's FormData, so you don't write e.preventDefault() or manual loading flags. After a successful action, React resets the form's uncontrolled fields.

    The supporting hooks:

    • useActionState(action, initialState) returns [state, formAction, isPending]. The action receives the previous state and the form data and returns the next state, which is handy for validation errors.
    • useFormStatus() from react-dom returns pending, data and more for the parent form, so it must be called in a component rendered inside the <form>, like a SubmitButton.
    • useOptimistic shows an optimistic value while an action is pending; once it settles, the UI shows the real state again.

    With a framework that supports Server Functions, the action can be a 'use server' function, and forms can even submit before hydration.

    What interviewers listen for
    • form action={fn} calls fn with FormData inside a transition
    • useActionState returns state, a wrapped action and isPending
    • useFormStatus reads the parent form, so call it in a child component
    • useOptimistic for optimistic UI during an action
    • Uncontrolled fields reset after a successful action

Prefer multiple choice? All 26 React MCQs with answers →

esc