Skip to content
Frontend

42 React Interview Questions and Answers

This focused guide turns RecallDeck’s curated React material into 42 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

40 min read42 detailed answersReviewed Aug 24, 2026
What to remember

Connect every framework answer to browser behavior, accessibility, performance, or user-visible state; that is where senior frontend judgment becomes visible.

Question set

42 detailed answers

01

What is React and what problem does it solve?

Short answer: React is a JavaScript library for building user interfaces through composition of reusable components. Its core idea is UI as a function of state: UI = f(state).

In detail:

React is a library (not a framework), developed by Facebook (Meta). It is responsible for the View layer and doesn't dictate how to organize routing, server requests, or state management — this gives flexibility but requires pulling in additional libraries.

Key principles:

  • Declarativeness — you describe what should be displayed for a given state, not how to mutate the DOM step by step.
  • Component-based — the UI is assembled from independent reusable blocks.
  • Unidirectional data flow — data flows top-down (from parent to children).
// We describe WHAT the UI looks like for a given state, not how to change it
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Clicks: {count}</button>;
}

⚠️ Gotcha: React is often called a "framework." In an interview it's more correct to say "a library for UI." Angular is a framework (routing, DI, forms, HTTP "out of the box"), whereas React only covers the View.

02

What does "React is declarative" mean?

Short answer: You describe the desired end result (the UI for a given state), and React itself figures out what changes to apply to the DOM. The imperative approach is manual, step-by-step DOM manipulation.

In detail:

// Imperatively (vanilla JS): we say HOW to change things
const btn = document.getElementById('btn');
let count = 0;
btn.addEventListener('click', () => {
  count++;
  btn.textContent = `Clicks: ${count}`; // manually updating the DOM
});
// Declaratively (React): we say WHAT to show
function Btn() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>Clicks: {count}</button>;
}

Advantages of declarativeness: fewer bugs from DOM and data getting out of sync, code is predictable and reads like a "snapshot" of the UI.

⚠️ Gotcha: Declarativeness isn't free — you pay for it with the work of the Virtual DOM and diffing. But in most applications that's a worthwhile trade-off for maintainability.

03

What is the component-based approach and what is an SPA?

Short answer: A component is a self-contained piece of UI with its own logic and markup. An SPA (Single Page Application) is an application that loads one HTML page and dynamically re-renders content without reloading the page.

In detail:

// Composition: small components are assembled into larger ones
function Avatar({ url }) { return <img className="avatar" src={url} />; }
function UserCard({ user }) {
  return (
    <div className="card">
      <Avatar url={user.avatar} />
      <span>{user.name}</span>
    </div>
  );
}
function App() {
  return <UserCard user={{ name: 'Anya', avatar: '/a.png' }} />;
}

In an SPA, navigation updates only the necessary part of the DOM (via client-side routing, e.g. React Router) instead of loading a new page from the server. This gives the feel of a "native" application.

⚠️ Gotcha: SPAs have downsides: worse baseline SEO (content is rendered by JS), a large initial bundle, and the need for hydration with SSR. Some of these problems are solved with SSR/SSG (Next.js).

04

What is JSX and how is it transpiled?

Short answer: JSX is syntactic sugar over element-creation calls. The browser doesn't understand JSX; Babel/SWC transpile it into React.createElement(...) (the classic runtime) or into jsx(...) from react/jsx-runtime (the new automatic runtime in React 17+).

In detail:

// We write JSX
const el = <h1 className="title">Hello, {name}!</h1>;

// Classic transpilation (before React 17):
const el = React.createElement('h1', { className: 'title' }, 'Hello, ', name, '!');

// New automatic runtime (React 17+), the import is added automatically:
import { jsx as _jsx } from 'react/jsx-runtime';
const el = _jsx('h1', { className: 'title', children: ['Hello, ', name, '!'] });

React.createElement returns a plain JS object (a React element) — a lightweight description of what should appear on screen: { type, props, key, ... }. This is NOT a DOM node.

JSX rules:

  • There must be a single root element (or <>...</> — a Fragment).
  • classclassName, forhtmlFor (because class/for are reserved words).
  • Attributes in camelCase: onClick, tabIndex.
  • Expressions go in {}, but only expressions (not statements: you can't use if, but you can use a ternary).
  • Components are capitalized (<MyComp />), otherwise React treats it as an HTML tag.
// JSX is an expression — you can assign it, return it, put it in an array
const items = list.map(x => <li key={x.id}>{x.name}</li>);

⚠️ Gotcha: With the new automatic runtime, importing React in the file is no longer required for JSX (it used to be needed because React.createElement was called). But if you use React.something directly, the import is still needed.

05

What is the Virtual DOM and why is it needed?

Short answer: The Virtual DOM (VDOM) is a lightweight representation of the UI as JS objects in memory. React keeps a VDOM; when state changes it builds a new VDOM, compares it with the old one (diffing), and applies only the minimal set of changes to the real DOM.

In detail:

Real DOM operations are expensive (style recalculation, reflow, repaint). Direct bulk DOM updates are slow. The VDOM is a layer of abstraction:

1. state changes
2. React calls the component → gets a new VDOM tree (JS objects)
3. Compares the new VDOM with the old one (reconciliation)
4. Computes the minimal diff
5. Applies ONLY the changes to the real DOM (batched)
// When count changes React does NOT re-render the whole <div>,
// it changes only the text node inside <span>
function App({ count }) {
  return (
    <div>
      <header>Title</header>          {/* untouched */}
      <span>{count}</span>           {/* only this updates */}
    </div>
  );
}

⚠️ Gotcha: A common misconception is "the VDOM is always faster than the direct DOM." That's not true: the VDOM adds overhead (building and comparing trees). Its value is in predictability and in freeing the developer from manually optimizing updates, not in absolute speed. A hand-written, pinpoint textContent = x is faster, but it doesn't scale.

06

How does reconciliation work?

Short answer: Reconciliation is the process of comparing the new VDOM tree with the old one to determine the minimal changes. React uses a heuristic O(n) algorithm, relying on two assumptions: elements of different types produce different trees, and node stability within lists is specified via key.

In detail:

An exact comparison of two trees is an O(n³) problem. React reduces it to O(n) with heuristics:

  1. Different element type → full subtree replacement. If a <div> becomes a <span>, React destroys the old node with all its contents (including the state of child components) and creates a new one.
// When isLoggedIn changes, React does NOT reuse the old component:
{isLoggedIn ? <AdminPanel /> : <LoginForm />}
// AdminPanel and LoginForm are different types, the subtree is recreated
  1. Same element type → props update. React keeps the node, changes only the attributes that changed, and recursively compares the children.

  2. Lists are compared by key. Without a key, React matches children by position, which leads to bugs when inserting/removing at the beginning.

⚠️ Gotcha: Conditionally rendering a component of the same type in different branches with DIFFERENT structure can unexpectedly preserve state. For example, two <input>s in both branches of a ternary at the same position reuse the DOM node and keep the entered text. This is solved with different keys.

07

Why is `key` needed and why shouldn't you use the array `index`?

Short answer: key helps React identify list elements across renders so it can correctly reuse, reorder, and remove nodes. Using index as a key breaks on insertion/removal/sorting, causing state bugs and inefficient updates.

In detail:

// GOOD: a stable unique id
{users.map(u => <UserRow key={u.id} user={u} />)}

// BAD: index as a key
{users.map((u, i) => <UserRow key={i} user={u} />)}

What breaks with index: if you insert an element at the start of the list, all the elements' indices shift. React thinks the element with key=0 "stayed the same" and reuses its DOM/state, even though the data is different.

// Bug demonstration: each input has internal state (uncontrolled)
function Todos({ items }) {
  return items.map((item, i) => (
    <li key={i}>
      {item.text}
      <input defaultValue="" /> {/* on insertion at the start the text will "move" to the wrong place */}
    </li>
  ));
}

When index is acceptable: the list is static, isn't reordered, elements aren't added/removed in the middle, and there's no local state/uncontrolled input.

⚠️ Gotcha: key must be unique among siblings, not globally. And key is not a prop: the component won't receive props.key. If you need the value inside — pass it as a separate prop.

08

What is the Fiber architecture (briefly)?

Short answer: Fiber is the reconciliation engine rewritten as of React 16. It represents each element as a "fiber" (a unit of work) and makes rendering interruptible: React can split the work into chunks, pause it, prioritize important updates, and continue later. This is the foundation of the Concurrent features.

In detail:

Before Fiber (the stack reconciler), rendering was synchronous and recursive — once it started, it couldn't be interrupted, which blocked the main thread on large trees.

Fiber splits the work into two phases:

  • Render/reconciliation phase — interruptible, no side effects: the fiber tree is built and the diff is computed. It can be paused, aborted, restarted.
  • Commit phase — synchronous, non-interruptible: changes are applied to the real DOM and effects are run.

This allows scheduling work by priority (animation is more important than data loading) — useTransition, Suspense, and time slicing are built on this.

⚠️ Gotcha: In the render phase a component may be called multiple times, or its result may be discarded. Therefore render must be pure (no side effects) — all effects go into useEffect/useLayoutEffect, which run in the commit phase.

09

Functional vs class components — what's the difference and why did people switch to functional?

Short answer: Class components are ES6 classes with lifecycle methods and this.state. Functional components are plain functions that get hooks for state and effects (as of React 16.8). The community switched to functional ones: less boilerplate, no confusion with this, better logic reuse via hooks.

In detail:

// Class component
class Counter extends React.Component {
  state = { count: 0 };
  increment = () => this.setState({ count: this.state.count + 1 });
  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}

// Functional — the same thing
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Problems with classes that hooks solved:

  • this is confusing (you need bind or arrow methods).
  • Lifecycle logic was hard to reuse (HOCs and render props led to "wrapper hell").
  • Related logic is scattered across different methods (componentDidMount/componentWillUnmount), while unrelated logic is mixed together in one method.

⚠️ Gotcha: Classes are not considered deprecated — old code on them works. But new code is written with functional components. Error boundaries are still implemented only with classes (there's no hook equivalent of componentDidCatch).

10

What's the difference between props and state?

Short answer: Props are input data passed to a component from the outside (from the parent), and they are read-only. State is the component's internal mutable state, which it manages itself. Changing either one triggers a re-render.

In detail:

Props State
Origin from the parent inside the component
Mutability immutable (read-only) mutable (via setState/setX)
Who changes it the parent the component itself
function Greeting({ name }) {          // name — a prop, from the outside
  const [count, setCount] = useState(0); // count — state, internal
  return <p>{name}: {count}</p>;
}

⚠️ Gotcha: You can't mutate props (props.name = 'x') — that breaks the unidirectional flow and won't trigger a re-render in the parent. If a child needs to change the parent's data — the parent passes down a callback (lifting state up).

11

What are unidirectional data flow, lifting state up, and props drilling?

Short answer: Data in React flows only top-down (from parent to children via props). Lifting state up is moving shared state up to the nearest common ancestor when several components need to share data. Props drilling is the problem of passing props through many intermediate layers that don't need them.

In detail:

// Lifting state up: state lives in the parent, children get the value and a callback
function Parent() {
  const [value, setValue] = useState('');
  return (
    <>
      <Input value={value} onChange={setValue} />
      <Preview value={value} />
    </>
  );
}

Props drilling is an anti-pattern where a prop "falls through" several levels:

// theme is only needed by Button, but we drag it through App → Page → Toolbar → Button
<Page theme={theme}><Toolbar theme={theme}><Button theme={theme} /></Toolbar></Page>

Solutions to props drilling: Context API, composition (passing children), a state manager.

⚠️ Gotcha: Don't immediately reach for Context the first time you pass something through 2 levels. First consider composition (children), which often eliminates drilling without global state.

12

Why did hooks appear and what are the rules for using them?

Short answer: Hooks (React 16.8) let you use state and other React features in functional components and reuse logic without classes and HOCs. Two rules: call hooks only at the top level (not in conditions/loops/nested functions) and only from React components or other hooks.

In detail:

Hooks removed class this binding, made stateful logic reusable through custom hooks instead of nested render props/HOCs, and let code for one effect stay together rather than being split across lifecycle methods.

Rules of hooks:

  1. Only at the top level. You can't call hooks inside conditions, loops, or nested functions.
// ❌ NOT ALLOWED
if (cond) { const [x, setX] = useState(0); }

// ✅ Correct
const [x, setX] = useState(0);
if (cond) { /* use x */ }
  1. Only from React functions (components or custom hooks), not from ordinary functions.

Why rule #1? React doesn't store state by variable name — it relies on the order in which hooks are called. Internally this is like an array/linked list: the first useState is slot 0, the second is slot 1, and so on. If you put a hook under a condition, the order shifts between renders, and React will associate state with the wrong hook.

// Internal model (simplified):
// render 1: [count, name, effect]  slots 0,1,2
// render 2 (if the useState under the if is skipped): [name, effect] — shift! bug

⚠️ Gotcha: The ESLint plugin eslint-plugin-react-hooks catches rule violations and missing dependencies — it's a must-have in a project. In an interview it's important to explain why the rule exists (call order), not just quote it.

13

How does useState work? What are batching, the asynchrony of setState, and functional updates?

Short answer: useState returns the current value and a setter. Calling the setter doesn't change the variable instantly — it schedules a re-render; within a single handler the updates are grouped (batching). If the new value depends on the previous one, use the functional form setX(prev => ...). Do heavy initialization lazily.

In detail:

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1); // count = 0, schedules 1
    setCount(count + 1); // count is STILL 0 in this closure, schedules 1
    // Result: count becomes 1, not 2!
  }

  function handleClickFixed() {
    setCount(prev => prev + 1); // 0 → 1
    setCount(prev => prev + 1); // 1 → 2
    // Result: 2 — the functional form sees the actual previous value
  }
}

Asynchrony: count is the value from the current render's closure; it doesn't change after the setter is called. The new value is available only in the next render.

Batching: several setX calls in one handler cause a single re-render, not several.

Lazy initialization: if the initial value requires expensive computation — pass a function, it will run only on the first render:

// ❌ The expensive function is called on EVERY render (the result is ignored)
const [data] = useState(expensiveInit());

// ✅ The function is called ONCE on mount
const [data] = useState(() => expensiveInit());

⚠️ Gotcha: setCount(count + 1) several times in a row is a classic mistake. Always use the functional form when the new value depends on the old one.

14

What is useEffect, why the dependency array, what is cleanup, and when is it called?

Short answer: useEffect runs side effects (requests, subscriptions, manual DOM operations) after render. The dependency array determines when the effect re-runs. The function returned from the effect is the cleanup; it's called before the next run of the effect and on unmount.

In detail:

useEffect(() => {
  // effect
  const sub = api.subscribe(id, setData);
  return () => sub.unsubscribe(); // cleanup
}, [id]); // dependencies

When it's called (depending on the dependency array):

useEffect(fn);          // after EVERY render
useEffect(fn, []);      // once after mount (cleanup on unmount)
useEffect(fn, [a, b]);  // after mount + when a or b changed

Order when dependencies update: cleanup of the previous effect → new effect.

Emulating class lifecycle:

  • componentDidMountuseEffect(fn, [])
  • componentDidUpdateuseEffect(fn, [deps])
  • componentWillUnmountreturn () => {} from useEffect(fn, [])

⚠️ Gotcha: useEffect is NOT needed for derived data. If a value can be computed from existing state/props during render — compute it right there, don't duplicate it into state via an effect.

// ❌ An extra effect and an extra re-render
const [fullName, setFullName] = useState('');
useEffect(() => setFullName(first + ' ' + last), [first, last]);

// ✅ Just compute during render
const fullName = first + ' ' + last;
15

What are the typical bugs associated with useEffect?

Short answer: Three classic ones: an infinite loop (the effect changes its own dependency), missing dependencies (stale data), and a stale closure (the effect captured an outdated value from an old render).

In detail:

1. Infinite loop:

// ❌ The effect changes data, data is in the dependencies → the effect runs again → infinitely
useEffect(() => {
  setData([...data, newItem]);
}, [data]);

// ✅ Functional update + remove data from the dependencies
useEffect(() => {
  setData(prev => [...prev, newItem]);
}, [newItem]);

2. Missing dependencies:

// ❌ userId is not in the dependencies — when userId changes the data won't be re-fetched
useEffect(() => { fetchUser(userId); }, []);
// ✅
useEffect(() => { fetchUser(userId); }, [userId]);

3. Stale closure:

// ❌ the interval captured count=0 forever (empty deps array)
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, []); // count is always 0 inside
// ✅ the functional form doesn't depend on the captured count
useEffect(() => {
  const id = setInterval(() => setCount(c => c + 1), 1000);
  return () => clearInterval(id);
}, []);

⚠️ Gotcha: Don't "silence" the linter with an empty dependency array just to remove the warning. Either honestly add the dependency, or apply a functional update / useRef. Ignoring the linter almost always hides a bug.

16

How does useEffect differ from useLayoutEffect?

Short answer: useEffect runs asynchronously after the browser has painted a frame (it doesn't block paint). useLayoutEffect runs synchronously after DOM mutations but before paint — use it when you need to measure/change the DOM before the user sees the frame (otherwise there will be flicker).

In detail:

// useLayoutEffect: measure the DOM and correct it before paint — no flicker
useLayoutEffect(() => {
  const { height } = ref.current.getBoundingClientRect();
  setTooltipPos(height); // the user won't see the intermediate state
}, []);

Order: DOM mutation → useLayoutEffect (synchronous, blocks paint) → the browser paints → useEffect (asynchronous).

⚠️ Gotcha: useLayoutEffect blocks painting, so heavy work in it hurts performance. By default use useEffect; switch to useLayoutEffect only when there's visual flicker due to DOM measurements. Also, useLayoutEffect produces a warning with SSR (it doesn't run on the server).

17

Why is useRef needed and how does it differ from state?

Short answer: useRef stores a mutable value in .current that persists across renders, but changing it does NOT trigger a re-render. It's used for accessing DOM nodes and for storing values that don't affect rendering (timers, previous values).

In detail:

function TextInput() {
  const inputRef = useRef(null);            // reference to the DOM
  const renderCount = useRef(0);            // a mutable value
  renderCount.current++;                    // doesn't trigger a re-render
  return <input ref={inputRef} onClick={() => inputRef.current.focus()} />;
}
useState useRef
Change → re-render yes no
Value across renders persists persists
Purpose data for the UI DOM references, hidden mutable values

⚠️ Gotcha: Don't use a ref for data that should be displayed — changing a ref won't re-render the UI, and the old value will stay on screen. And don't read/write ref.current during render (only in handlers and effects) — that violates render purity.

18

What's the difference between useMemo and useCallback? When are they needed and when are they harmful?

Short answer: useMemo caches the result of a computation, useCallback caches the function itself (it's useMemo(() => fn, deps)). Both are needed to preserve referential equality across renders — for heavy computations and to prevent unnecessary re-renders of memoized children. Premature memoization is harmful: it adds overhead and complicates the code.

In detail:

// useMemo — caches a value
const sorted = useMemo(() => bigList.sort(cmp), [bigList]);

// useCallback — caches a function (a stable reference)
const handleClick = useCallback(() => doSomething(id), [id]);

// Equivalent:
const handleClick = useMemo(() => () => doSomething(id), [id]);

Why referential stability is needed: on every render objects/functions are created anew ({} !== {}, () => {} !== () => {}). If such an object/function is passed to a React.memo component or into the dependencies of useEffect, memoization breaks / the effect re-runs.

const Child = React.memo(ChildComp);
// Without useCallback, onClick is a new reference each render → memo is useless
<Child onClick={useCallback(() => {}, [])} />

When it's REALLY needed:

  • A heavy computation (sorting/filtering large arrays) — useMemo.
  • Passing a function/object to a React.memo child.
  • Passing into the dependencies of another hook.

When it's HARMFUL: wrapping everything indiscriminately. Memoization itself costs memory and dependency comparison. For cheap computations and components without React.memo it's just extra code and overhead with no benefit.

⚠️ Gotcha: useMemo/useCallback are a hint, not a guarantee: React may discard the cache. And memoization is useless if the dependencies change every render (for example, you pass into deps an object that you create anew yourself).

19

What does React.memo do and when does it help?

Short answer: React.memo is a HOC that memoizes a component's render result and skips re-rendering if the props haven't changed (by shallow comparison). It helps for "heavy" components that frequently receive the same props.

In detail:

const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  return items.map(i => <Row key={i.id} {...i} />);
});

Shallow comparison: React compares each prop via Object.is (essentially ===). Primitives are compared by value, objects/arrays/functions by reference.

// memo won't kick in: style is a new object each render
<ExpensiveList items={items} style={{ color: 'red' }} />
// You need to extract it: const style = useMemo(() => ({ color: 'red' }), []);

⚠️ Gotcha: React.memo is useless (and even harmful due to the extra comparison) if the component almost always receives new props (new objects/functions/children). Its effect is often nullified by passing inline objects and arrow functions. The combo memo + useCallback/useMemo on the props works as a pair.

20

What is the Context API (useContext) and what is its problem?

Short answer: Context lets you pass data through the component tree without props drilling. useContext reads the value of the nearest Provider. The main problem: when the context value changes, ALL consumers re-render, even if they only care about part of the data.

In detail:

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}
function Button() {
  const theme = useContext(ThemeContext); // no drilling
  return <button className={theme}>OK</button>;
}

The re-render problem: when the Provider's value changes, all components calling useContext re-render. If you put a large object in value, any change re-renders all consumers.

Mitigation: split contexts by meaning, memoize value, move rarely-changing data out separately.

⚠️ Gotcha: Context is a delivery mechanism (DI), not a state management mechanism. It doesn't optimize re-renders and doesn't normalize data. For frequent updates and complex logic it doesn't replace a state manager (Redux/Zustand). Also, an inline value={{...}} creates a new object every render — memoize it.

21

When should you use useReducer instead of useState?

Short answer: useReducer is a good fit when state logic is complex: several related fields, state transitions depend on the previous state, or one action changes multiple values. It centralizes the update logic in a pure reducer function.

In detail:

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + 1 };
    case 'reset':     return { ...state, count: 0 };
    default:          throw new Error('unknown action');
  }
}
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return <button onClick={() => dispatch({ type: 'increment' })}>{state.count}</button>;
}

Advantages: update logic in one place, easy to test (a pure function), dispatch is stable by reference (no useCallback needed).

⚠️ Gotcha: For simple state (one boolean/number) useReducer is over-engineering. A reducer must be pure: no mutating its arguments, no requests, no Date.now()/Math.random() inside.

22

What are custom hooks and why are they needed?

Short answer: A custom hook is an ordinary function whose name starts with use and which calls other hooks internally. They let you reuse stateful logic across components without duplication and without HOCs/render props.

In detail:

function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle];
}

// Reuse in any component
function Modal() {
  const [open, toggleOpen] = useToggle();
  return <button onClick={toggleOpen}>{open ? 'Close' : 'Open'}</button>;
}

Naming rule: the use prefix is mandatory — it tells the linter that the function contains hooks, so the rules of hooks apply.

⚠️ Gotcha: A custom hook does NOT share state between components — every call creates an independent instance of state. If two components need shared state, lift it higher or use Context/a store. Hooks reuse logic, not data.

23

What is the lifecycle of class components (terminology)?

Short answer: Three phases — mounting, updating, unmounting. The main methods: constructor, render, componentDidMount, componentDidUpdate, componentWillUnmount, shouldComponentUpdate, getDerivedStateFromProps, componentDidCatch.

In detail:

class C extends React.Component {
  constructor(props) { super(props); this.state = {}; }   // init
  componentDidMount()   { /* after first render: requests, subscriptions */ }
  shouldComponentUpdate(nextProps, nextState) { return true; } // optimization
  componentDidUpdate(prevProps) { /* after an update */ }
  componentWillUnmount() { /* cleanup: unsubscribe, timers */ }
  render() { return <div />; }
}

Mapping to hooks:

  • componentDidMount + componentDidUpdate + componentWillUnmountuseEffect
  • shouldComponentUpdateReact.memo
  • internal state this.stateuseState/useReducer

⚠️ Gotcha: useEffect(fn, []) is not identical to componentDidMount: the effect fires after paint (asynchronously), whereas componentDidMount runs synchronously before paint. The exact timing equivalent is useLayoutEffect. Also, the componentWillMount/componentWillReceiveProps methods are deprecated (legacy/unsafe).

24

How do controlled components differ from uncontrolled ones?

Short answer: In a controlled component the form value is stored in React state and set via value + onChange — React is the "single source of truth." In an uncontrolled component the value lives in the DOM itself, and React reads it through a ref when needed (defaultValue).

In detail:

// Controlled: React manages the value
function Controlled() {
  const [val, setVal] = useState('');
  return <input value={val} onChange={e => setVal(e.target.value)} />;
}

// Uncontrolled: the DOM holds the value, we read it via a ref
function Uncontrolled() {
  const ref = useRef();
  return (
    <>
      <input defaultValue="" ref={ref} />
      <button onClick={() => console.log(ref.current.value)}>OK</button>
    </>
  );
}

Controlled gives full control (on-the-fly validation, formatting, conditional disabling) but requires a re-render on every keystroke. Uncontrolled is simpler and more performant for large forms, but harder for dynamic validation.

⚠️ Gotcha: You can't mix them: passing value without onChange makes the field "read-only" and React emits a warning. Switching value from undefined to a string (controlled ↔ uncontrolled) is a common source of warnings.

25

What causes a component to re-render, and how does React decide what to repaint?

Short answer: A component re-renders when its own state changes, when its parent re-renders, when a subscribed context value changes, and (for classes) when it receives new props. A re-render means the component function is called again and the VDOM is rebuilt — but this does NOT necessarily change the real DOM.

In detail:

Re-render triggers:

  1. Calling a state setter (setX) — if the new value differs (Object.is).
  2. A parent re-render — by default this re-renders all children (unless React.memo).
  3. A change in the value of a subscribed useContext.
function Parent() {
  const [n, setN] = useState(0);
  return (
    <>
      <button onClick={() => setN(n + 1)}>+</button>
      <Child /> {/* re-renders on every click, even though its props didn't change */}
    </>
  );
}

Key principle: re-render ≠ DOM update. React re-renders (calls the function for) a component, builds a new VDOM, compares it with the old one, and touches the DOM ONLY where there are actual changes. That's why an "extra" re-render isn't always expensive — it's expensive when the tree is large or the computations are heavy.

⚠️ Gotcha: A common misconception is that "passing a prop causes the child to re-render." In reality the child re-renders because the parent re-rendered (not because of the props themselves). React.memo breaks this link by comparing props.

26

How do you optimize the performance of a React application?

Short answer: Profile first, optimize surgically. Tools: React.memo/useMemo/useCallback against unnecessary re-renders, code splitting via lazy+Suspense, virtualization of long lists, correct keys, pushing state down / hoisting out "expensive" children, the React DevTools Profiler.

In detail:

1. Reducing re-renders: React.memo for expensive children, useMemo/useCallback for stable props, lifting/lowering state.

// Pushing state down: typing doesn't re-render the heavy list
function Page() {
  return <><SearchBox /><ExpensiveList /></>; // instead of state in Page
}

2. Code splitting (lazy loading):

const Settings = React.lazy(() => import('./Settings'));
<Suspense fallback={<Spinner />}>
  <Settings />
</Suspense>

3. List virtualization (react-window/react-virtualized): render only the visible items out of thousands.

4. Profiler (React DevTools Profiler): find what re-renders and why, measure commit times.

⚠️ Gotcha: Measure first, then optimize. Prematurely wrapping everything in useMemo/memo complicates the code and often slows it down (the overhead of comparisons). Most re-renders are cheap. The Profiler reveals the real bottlenecks.

27

When is useState/Context enough, and when do you need Redux/Zustand/MobX?

Short answer: Local state (useState/useReducer) is enough for the state of a single component. Context is for rarely-changing global data (theme, locale, current user). An external store (Redux/Zustand/MobX) is needed for complex, frequently-updated global state with many consumers, debugging, and predictability.

In detail:

Solution When
useState/useReducer state of a single component/subtree
Context rarely-changing global data (theme, auth, i18n)
Zustand/Jotai global state with minimal boilerplate, fine-grained subscriptions
Redux (Toolkit) large app, strict predictability, DevTools, middleware
MobX reactive observable approach, less boilerplate
React Query/RTK Query server state (cache of API data)
// Zustand — minimal boilerplate, subscribe only to the slice you need
const useStore = create(set => ({ count: 0, inc: () => set(s => ({ count: s.count + 1 })) }));
const count = useStore(s => s.count); // re-renders only when count changes

⚠️ Gotcha: The key distinction is client state vs server state. Data from the server (lists, profiles) is a cache with background synchronization, invalidation, and loading statuses. It's better to keep it not in Redux by hand, but in React Query/RTK Query. Don't dump everything into a global store.

28

Explain the concepts of Redux and Redux Toolkit.

Short answer: Redux is a predictable state container: a single store, changes only through dispatch(action), and the new state computed by pure reducer functions immutably. Redux Toolkit (RTK) is the official toolset that removes boilerplate (createSlice, built-in Immer, a preconfigured store).

In detail:

Core concepts:

  • Store — a single state tree (single source of truth).
  • Action — an object { type, payload } describing "what happened."
  • Reducer — a pure function (state, action) => newState, with no mutations or side effects.
  • Dispatch — sending an action to the store, the only way to change state.
// Classic Redux
function reducer(state = { count: 0 }, action) {
  switch (action.type) {
    case 'INC': return { ...state, count: state.count + 1 }; // immutable
    default: return state;
  }
}
// Redux Toolkit — the same without boilerplate (Immer lets you "mutate" a draft)
const slice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: { inc: (state) => { state.count += 1; } }, // Immer → immutable under the hood
});
export const { inc } = slice.actions;

Flow: UI dispatch(action) → reducer computes the new state → subscribers (components) re-render.

⚠️ Gotcha: A reducer must be pure and immutable — no state.x = ... (in plain Redux), no requests, no mutations. In RTK "mutation" is syntactically allowed, but it's Immer creating a new immutable object under the hood. Side effects (requests) go in middleware (thunk/saga), not in the reducer.

29

What are RTK Query / React Query, and what's the difference between client state and server state?

Short answer: RTK Query and React Query (TanStack Query) are libraries for managing server state: caching API responses, deduplicating requests, background updates, invalidation, loading/error statuses. Server state differs from client state in that the server owns it, it's asynchronous, and it can go stale.

In detail:

// React Query — cache, statuses, refetch out of the box
function Users() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
  });
  if (isLoading) return <Spinner />;
  return <List items={data} />;
}

Client state (theme, whether a modal is open, a form) is synchronous and entirely ours. Server state (API data) is asynchronous, shared between clients, and needs caching and synchronization.

⚠️ Gotcha: Storing server data in Redux by hand (useEffect + dispatch) is reinventing the cache: you'll have to write loading, invalidation, deduplication, and race handling yourself. Specialized libraries do this more reliably. Leave Redux/Zustand for client state.

30

What is the stale closure problem in hooks?

Short answer: A stale closure is when a function (in an effect, callback, or timer) has "captured" variable values from an old render and keeps using outdated data. It happens because every render creates a new closure with its own copies of state/props.

In detail:

function Timer() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // ALWAYS 0 — closure from the first render
    }, 1000);
    return () => clearInterval(id);
  }, []); // empty array → the callback sees count only from the initial render
}

Solutions:

  • Functional update: setCount(c => c + 1) — doesn't depend on the captured count.
  • Add the value to the dependencies (the effect is recreated with an up-to-date closure).
  • useRef to hold an always-current value: countRef.current.
const countRef = useRef(count);
countRef.current = count; // update it on every render
// inside the interval we read countRef.current — always fresh

⚠️ Gotcha: Stale closures are especially treacherous with an empty dependency array and long-lived callbacks (intervals, event handlers, subscriptions). The dependencies linter helps detect capturing of stale values.

31

What is automatic batching in React 18?

Short answer: Batching is grouping several state updates into a single re-render. Before React 18, batching worked only inside React event handlers. In React 18 it became automatic everywhere — in promises, setTimeout, native handlers, and async functions.

In detail:

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 17 and 18: ONE re-render (batching in the handler)
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 17: TWO re-renders; React 18: ONE (automatic batching)
}, 1000);

If you need to force an update to apply synchronously outside a batch — there's flushSync (use it rarely, it breaks the optimization).

⚠️ Gotcha: Automatic batching is enabled when you use createRoot (the new React 18 API). If the app mounts with the old ReactDOM.render, the behavior stays as in React 17. Most code isn't broken by batching, but if you relied on an intermediate re-render between two setX calls in a setTimeout — the behavior will change.

32

What are concurrent features (Suspense, useTransition)?

Short answer: React 18's concurrent features let React interrupt and prioritize rendering. useTransition marks updates as non-urgent (they don't block input). Suspense declares a boundary with a fallback while a component is "waiting" (lazy code or data).

In detail:

// useTransition: heavy filtering doesn't freeze the input field
function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function onChange(e) {
    setQuery(e.target.value);                 // urgent: the input responds instantly
    startTransition(() => {                    // non-urgent: can be interrupted
      setResults(filterHugeList(e.target.value));
    });
  }
  return <>{isPending && <Spinner />}<input value={query} onChange={onChange} /></>;
}
// Suspense: show a fallback while a lazy component or data is loading
<Suspense fallback={<Spinner />}>
  <LazyDashboard />
</Suspense>

useDeferredValue is a related hook: it defers updating a value so as not to block an urgent render.

⚠️ Gotcha: Concurrent features don't make code "magically fast" — they change rendering priorities, giving the UI responsiveness. Suspense for data fetching works fully with frameworks/libraries that support it (Next.js, React Query, RSC), not with just any fetch in useEffect.

33

What are error boundaries?

Short answer: An error boundary is a component (class-only) that catches JS render errors in its subtree, logs them, and shows a fallback UI instead of crashing the whole app. It's implemented via getDerivedStateFromError and componentDidCatch.

In detail:

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() { return { hasError: true }; }
  componentDidCatch(error, info) { logError(error, info); }
  render() {
    return this.state.hasError ? <h1>Something went wrong</h1> : this.props.children;
  }
}
// Usage
<ErrorBoundary><RiskyComponent /></ErrorBoundary>

⚠️ Gotcha: An error boundary does NOT catch errors: in event handlers (use try/catch there), in asynchronous code (setTimeout, promises), in SSR, or errors in the boundary itself. There is no hook equivalent — this is the only case where a class component is still needed (or the react-error-boundary library).

34

What are Portals and Fragments?

Short answer: A Portal renders child elements into a DOM node outside the parent's hierarchy (for modals, tooltips, popups) while preserving the React context and event bubbling. A Fragment (<>...</>) groups elements without an extra DOM wrapper.

In detail:

// Portal: the modal renders into body, but logically it's the component's child
function Modal({ children }) {
  return ReactDOM.createPortal(children, document.getElementById('modal-root'));
}

// Fragment: return several elements without an extra <div>
function List() {
  return (
    <>
      <li>One</li>
      <li>Two</li>
    </>
  );
}

Why a portal: to escape a parent's overflow: hidden/z-index for overlays. Important: events bubble along the React tree (through the portal-owning parent), not the DOM tree.

⚠️ Gotcha: A Fragment with a key must be written in full form <React.Fragment key={id}>; the short <> doesn't accept attributes. A Portal preserves the React context and event bubbling along the React tree — this is convenient but sometimes surprising (a click inside a portal modal will bubble up to the logical parent).

35

How do CSR, SSR, and SSG differ, what is hydration, and where does Next.js fit in?

Short answer: CSR (Client-Side Rendering) — the HTML is empty, JS draws everything in the browser. SSR (Server-Side Rendering) — the server returns ready-made HTML on every request. SSG (Static Site Generation) — the HTML is generated ahead of time at build. Hydration is the process of "reviving" the server HTML on the client: React attaches handlers and takes over. Next.js is a framework on top of React that provides SSR/SSG/ISR, routing, and optimizations out of the box.

In detail:

When the HTML is rendered Pros Cons
CSR in the browser simplicity, cheap poor SEO, slow first paint
SSR on the server, on every request SEO, fast content, fresh data server load
SSG at build time maximally fast, cheap to serve data "freezes" (needs ISR/rebuild)

Hydration: the server sent ready-made HTML → React on the client builds a VDOM from the same tree and "attaches" event handlers. Before hydration the page is visible but not interactive.

// Next.js App Router: server components by default,
// 'use client' marks a client (interactive) component
'use client';
export default function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

⚠️ Gotcha: Hydration mismatch — if the server HTML differs from the client's first render (e.g., you used Date.now(), window, random values, or locale), React throws an error and may re-render. That's why the first client render must match the server's.

36

Why is immutability important in React?

Short answer: React detects changes through a shallow comparison by reference (Object.is). If you mutate state directly, the reference doesn't change — React won't "see" the change and won't re-render (or will re-render incorrectly). Immutability (creating new objects/arrays) makes changes detectable and predictable.

In detail:

// ❌ Mutation — same reference, React won't see the change
const [user, setUser] = useState({ name: 'Anya', age: 20 });
user.age = 21;
setUser(user); // same reference → a re-render may not happen

// ✅ New object
setUser({ ...user, age: 21 });

// Arrays:
setItems([...items, newItem]);          // not push
setItems(items.filter(i => i.id !== x)); // not splice
setItems(items.map(i => i.id === x ? { ...i, done: true } : i));

Immutability also makes React.memo/useMemo/PureComponent work correctly (they compare by reference) and simplifies debugging (time-travel in Redux, a clear state history).

⚠️ Gotcha: A shallow copy ({...obj}) doesn't clone nested objects — to update a deeply nested field you need to copy the whole chain down to it (or use Immer / useImmer). Mutating a nested object inside a "copied" parent is a common hidden bug.

37

How does React differ from Vue and Angular (briefly)?

Short answer: React is a minimalist library for the View, with JSX and explicit control over updates; its ecosystem is assembled from third-party packages. Vue is a progressive framework with templates and built-in reactivity, with a gentle learning curve. Angular is a full-fledged framework (TypeScript, DI, routing, forms, RxJS) with a rigid structure.

In detail:

React Vue Angular
Type library (View) progressive framework full framework
Templates JSX (JS) HTML templates + directives HTML templates + decorators
Reactivity explicit (setState/hooks) automatic (proxy) RxJS / signals + zone.js
Ecosystem third-party, flexible official (Router, Pinia) everything "out of the box"
Learning curve medium low high

⚠️ Gotcha: The main conceptual difference: in React reactivity is explicit — you call the setter yourself, and React re-renders "downward." Vue tracks dependencies automatically (reactive proxies), and a component knows which data it reads. That's why manual memoization is needed less often in Vue.

38

Why is the Virtual DOM needed?

Short answer: To provide a declarative model (we describe the UI as a function of state) without manual and inefficient manipulation of the real DOM. The VDOM lets React compute the minimal set of changes and apply them in a batch.

In detail: The value of the VDOM lies not so much in speed as in abstraction: the developer describes the desired result, and React handles reconciling the old and new trees and surgically updating the DOM. This eliminates a whole class of UI/data desync bugs.

⚠️ Gotcha: Don't claim that "the VDOM is faster than the DOM." The correct statement: the VDOM makes updates predictable and fast enough, freeing you from manual optimization. There are approaches without a VDOM (Svelte compiles to direct DOM operations, SolidJS uses fine-grained reactivity).

39

Why are hooks needed, and what was wrong with classes?

Short answer: Hooks enable reuse of stateful logic (custom hooks instead of HOCs/render props), remove the confusion with this, and group related logic together. Classes suffered from "wrapper hell," logic smeared across lifecycle methods, and difficulties with this.

In detail: In classes a single feature (e.g., a subscription) was split across componentDidMount + componentWillUnmount, while a single method mixed unrelated logic. Hooks (useEffect with cleanup) let you keep related code together and extract it into reusable useX hooks.

⚠️ Gotcha: Hooks don't "abolish" classes and don't automatically make code better. Incorrect use (a giant useEffect, breaking the rules, forgotten dependencies) gives rise to new bugs. It's important to understand the "order of hook calls" model.

40

Why can't you mutate state directly?

Short answer: Because React compares state by reference. A mutation doesn't change the reference — React won't detect the change and won't re-render the UI (or will re-render inconsistently), and optimizations (memo, PureComponent) will break.

In detail: Direct assignment state.x = ... bypasses the setter, so React does not schedule a render. It also changes an object owned by an already completed render: older closures and memoized children can suddenly observe different data under the same reference. Create a new object or array and pass it to the setter; the new reference makes the update observable and preserves state as an immutable render snapshot.

⚠️ Gotcha: Even if you call setState with the same object after a mutation, a re-render may not happen (same reference). And React.StrictMode in dev helps surface unsafe mutations by calling render/effects twice.

41

When does useMemo do harm?

Short answer: When the computation is cheap, the component isn't wrapped in React.memo, or the dependencies change every render anyway. Then memoization only adds memory and dependency comparison, complicates the code, and yields no gain — it's premature optimization.

In detail: useMemo itself costs resources: storing a cache + comparing the dependency array on every render. If the computation is a + b, wrapping it in useMemo costs more than just computing it. The benefit appears only for genuinely heavy computations or for preserving referential stability that someone actually needs (a memoized child, a hook's deps).

⚠️ Gotcha: Wrapping everything in useMemo/useCallback "just in case" is a common anti-pattern. Profiler first, then surgical optimization. The exception is the new React Compiler (if enabled), which memoizes automatically, making manual wrappers unnecessary.

42

What does "React is a function from state to UI" mean?

Short answer: The formula UI = f(state): given the same state, a component always returns the same UI description. React, on receiving new state, calls f again and reconciles the result with the screen. The render must be pure.

In detail: This is React's mental model: a component is a pure function that transforms input (props + state) into a tree of elements. You don't need to think "how do I change the DOM from A to B" — you describe what the UI looks like for the current state, and React handles the transition.

// Same state → same output (purity)
function View({ user }) {
  return <h1>{user ? `Hello, ${user.name}` : 'Sign in'}</h1>;
}

⚠️ Gotcha: From "a function of state" follows the requirement that the render phase be pure: no side effects, no mutating props/state, no requests directly in the component body. All of that goes in event handlers and effects. StrictMode calls render twice in dev to surface impurity.

Source notes

References and review policy

RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going

RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS