Ch. 03

React

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

77 interview questions26 quiz questions3 notes
your progress0%

Notes in this chapter

Filter all notes →
read ✓React · mid

useEffect Pitfalls Interviewers Love to Ask

Stale closures, missing cleanup, fetch races, Strict Mode's double run, infinite loops and effects you don't need, plus how useLayoutEffect differs in timing.

~7 min readread →
read ✓React · mid

React Reconciliation & Keys: What Really Happens

Render vs commit, the diffing rules React relies on, why index keys break inputs, how a key resets state, and what Fiber and batching change about updates.

~7 min readread →

Top 77 React interview questions most asked first

  1. 1.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?

  2. 2.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?

  3. 3.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
  4. 4.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
  5. 5.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?

  6. 6.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?

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

  8. 8.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
  9. 9.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
  10. 10.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?

  11. 11.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?

  12. 12.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
  13. 13.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
  14. 14.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
  15. 15.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
  16. 16.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?

  17. 17.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
  18. 18.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?

  19. 19.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
  20. 20.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
  21. 21.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
  22. 22.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
  23. 23.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
  24. 24.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?

  25. 25.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?

  26. 26.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
  27. 27.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
  28. 28.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
  29. 29.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
  30. 30.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
  31. 31.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
  32. 32.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
  33. 33.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
  34. 34.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
  35. 35.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
  36. 36.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?

  37. 37.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
  38. 38.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
  39. 39.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
  40. 40.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
  41. 41.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
  42. 42.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
  43. 43.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?

  44. 44.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
  45. 45.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
  46. 46.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
  47. 47.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
  48. 48.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
  49. 49.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
  50. 50.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
  51. 51.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
  52. 52.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
  53. 53.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
  54. 54.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
  55. 55.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
  56. 56.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
  57. 57.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
  58. 58.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
  59. 59.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
  60. 60.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?

  61. 61.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?

  62. 62.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?

  63. 63.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?

  64. 64.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?

  65. 65.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
  66. 66.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?

  67. 67.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?

  68. 68.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?

  69. 69.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
  70. 70.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
  71. 71.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?

  72. 72.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
  73. 73.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
  74. 74.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
  75. 75.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
  76. 76.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
  77. 77.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
esc