Skip to the questions

Frontend Developer (React) · Fresher to 3 years

React interview questions

The 40 questions that come up most in React and TypeScript interviews. Each one has a short answer you can say out loud in about 30 seconds, and the full explanation, with tested React 19 code, for when you need it.

Read the quick answers

4 Fundamentals · 6 Rendering · 5 Core hooks · 6 More hooks · 5 Patterns · 2 State · 2 Performance · 2 Routing · 6 TypeScript · 2 TS + ReactUpdated 27 Oct 2026

When is your interview?

01React fundamentals

0/4 got it

Q01Not marked yetFresherAsked very oftenConcept

What is React, and why is it called a library rather than a framework?

Say this first~30 sec to say

React is a JavaScript library for building user interfaces from components: functions that take data and return what the UI should look like. You describe the UI declaratively for the current state, and React updates the DOM when that state changes. It's a library because it only handles the view: routing, data fetching, forms and global state come from packages you choose. A framework such as Angular ships all of those and decides your app's structure.

Could you say it?
Full answer2 min read

Declarative vs imperative

JSX
// Imperative, plain JavaScript: you change the DOM step by step
const button = document.querySelector("#like");
let likes = 0;
button.addEventListener("click", () => {
  likes += 1;
  button.textContent = `Likes: ${likes}`;
});

// Declarative, React: describe the UI for any value of likes, and React updates the DOM
function LikeButton() {
  const [likes, setLikes] = useState(0);
  return <button onClick={() => setLikes(likes + 1)}>Likes: {likes}</button>;
}

With two or three pieces of state, the imperative version quickly becomes a tangle of "if this changed, also update that". The declarative version stays the same size.

Library vs framework

ConcernReact (library)Angular (framework)Next.js (framework built on React)
Rendering the UIYesYesYes, via React
RoutingPick one: React Router, TanStack RouterBuilt inBuilt in: file-based
Data fetchingPick one: fetch, TanStack Query, SWRBuilt in: HttpClientBuilt in: server components, caching
FormsPick one, or plain stateBuilt inReact's, plus server actions
Project structureUp to youPrescribedPrescribed folders

The informal test: you call a library's code; a framework calls yours.

The ideas React is built on: components, one-way data flow (question 4), a virtual DOM with reconciliation (question 2), and hooks for state and side effects.

Follow-ups

  • Is Next.js a library or a framework? A framework built on React: it adds routing, server rendering, data fetching and build tooling (question 32).
  • What is React Native? The same component model, rendering to native iOS and Android views instead of the browser DOM.
Q02Not marked yetFresherAsked very oftenConcept

What is the virtual DOM, and how does reconciliation decide what to change in the real DOM?

Say this first~30 sec to say

The virtual DOM is a lightweight JavaScript description of the UI: the tree of elements your components return. When state changes, React re-renders the affected components into a new tree, compares it with the previous one (reconciliation, or diffing), and applies only the differences to the real DOM, which is slow to touch. To make diffing fast, React assumes that elements of a different type produce different trees, and uses keys to match list items.

Could you say it?
Full answer3 min read

Real DOM vs virtual DOM

Real DOMVirtual DOM
What it isThe browser's live tree of nodesPlain JavaScript objects describing the UI
Updating itCan trigger layout and repaintCheap: it's just objects
Who changes itYour code, or React on your behalfReact, on every render

The diffing rules

  1. Different element type (<div> becomes <section>, or <Profile> becomes <Settings>): React throws away the old subtree, state included, and builds a new one.
  2. Same DOM element type: React keeps the node and updates only the attributes that changed.
  3. Same component type: React keeps the component instance and its state, and passes the new props.
  4. Lists: React matches children by key (question 6).

Rule 1 has a visible consequence. Changing only the wrapper element wipes the state inside it:

JSX
import { useState } from "react";

function NameField() {
  const [name, setName] = useState("");
  return <input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} />;
}

export default function Form() {
  const [boxed, setBoxed] = useState(false);
  return (
    <>
      <button onClick={() => setBoxed(!boxed)}>Toggle box</button>
      {boxed ? <section><NameField /></section> : <div><NameField /></div>}
    </>
  );
}

Type a name, then press Toggle box: the input empties. The <div> became a <section>, so React rebuilt everything inside it, including NameField and its state.

Fiber, in one line: since React 16, the reconciler (called Fiber) splits rendering into small units of work, so React can pause, prioritise urgent updates such as typing, and resume.

Follow-ups

  • Is the virtual DOM faster than the real DOM? Not by itself: it's extra work. Its value is letting you write declarative code while React batches and minimises the real DOM updates. Frameworks such as Svelte and Solid skip it and still update efficiently.
  • Is the virtual DOM the same as the shadow DOM? No. The shadow DOM is a browser feature that encapsulates the markup and styles of web components.
Q03Not marked yetFresherAsked very oftenConcept

What is JSX, what does it compile to, and why must a component return a single root element or a Fragment?

Say this first~30 sec to say

JSX is a syntax extension that lets you write HTML-like markup inside JavaScript. A compiler such as Babel, esbuild or SWC turns each tag into a function call that returns a plain element object describing that piece of UI. A component must return a single root because a JavaScript function returns one value. To return siblings without adding a wrapper element to the DOM, group them in a Fragment, written <>…</>.

Could you say it?
Full answer3 min read

What the compiler produces

JSX
const title = <h1 className="title">Hello, {name}</h1>;
const list = <><li>One</li><li>Two</li></>;

With the modern JSX transform, which React 19 requires, that becomes:

JavaScript
import { Fragment, jsx, jsxs } from "react/jsx-runtime";

const title = jsxs("h1", { className: "title", children: ["Hello, ", name] });
const list = jsxs(Fragment, { children: [jsx("li", { children: "One" }), jsx("li", { children: "Two" })] });

The compiler adds the import itself, which is why React 19 files no longer need import React from "react" just to use JSX. Older tutorials show the classic transform, React.createElement("h1", { className: "title" }, "Hello, ", name), and that import at the top of every file.

Why one root

JSX
function Bad() {
  return <h1>Title</h1><p>Text</p>;    // a syntax error: two values
}

function Good() {
  return (
    <>
      <h1>Title</h1>
      <p>Text</p>
    </>
  );
}

A Fragment groups children without putting an extra <div> into the page, which keeps the DOM clean and avoids breaking layouts such as table rows or flex children. Use the long form, <Fragment key={item.id}>, when a Fragment needs a key inside a list.

JSX differs from HTML in a few places

HTMLJSXWhy
classclassNameclass is a reserved word in JavaScript
forhtmlForfor is reserved too
onclick="..."onClick={handleClick}camelCase names, and a function, not a string
style="color: red"style={{ color: "red" }}An object of camelCased properties
<img><img />Every tag must be closed
{expression}Any JavaScript expression, but not statements like if or for

Follow-ups

  • Can browsers run JSX? No. It must be compiled first, which build tools such as Vite and Next.js do for you.
  • Is JSX safe from XSS? Values inside {} are escaped before they reach the DOM. dangerouslySetInnerHTML bypasses that, which is why it carries the scary name.
Q04Not marked yetFresherAsked very oftenConcept

How are props different from state, and what does one-way data flow mean for passing data between components?

Say this first~30 sec to say

Props are inputs a parent passes to a child, like function arguments: the child reads them but must never change them. State is data a component owns and updates through its setter, which makes React re-render. With one-way data flow, data travels down the tree as props. When a child needs something to change, the parent passes down a callback; the child calls it, the parent updates its state, and the new value flows back down.

Could you say it?
Full answer3 min read
JSX
import { useState } from "react";

function QuantityPicker({ quantity, onChange }) {        // props: read-only inputs
  return (
    <div>
      <button onClick={() => onChange(quantity - 1)} disabled={quantity === 1}>-</button>
      <span>{quantity}</span>
      <button onClick={() => onChange(quantity + 1)}>+</button>
    </div>
  );
}

export default function CartItem() {
  const [quantity, setQuantity] = useState(1);            // state: owned here
  return (
    <>
      <QuantityPicker quantity={quantity} onChange={setQuantity} />
      <p>Total: ₹{quantity * 499}</p>
    </>
  );
}

QuantityPicker never changes quantity itself. It asks its parent to, through onChange, and both the picker and the total update from the same state.

PropsState
Comes fromThe parentThe component itself
Can the component change it?NoYes, through its setter
Changing it causesA re-render of the parent, which passes new propsA re-render of this component and its children
LikeFunction argumentsA component's private memory

React 19: defaults without defaultProps

React 19 removed propTypes and defaultProps for function components. Use default parameter values, and TypeScript for type checks:

JSX
function Avatar({ name, size = 48 }) {          // the default lives in the destructuring
  return <img alt={name} width={size} height={size} src={`/avatars/${name}.png`} />;
}

One-way vs two-way binding: Angular's [(ngModel)] and Vue's v-model sync an input and a variable in both directions automatically. React makes both directions explicit: value flows down, and onChange sends changes up. More typing, but you can always trace who changed what.

Follow-ups

  • What happens if a child mutates a prop object? Nothing re-renders, and the parent's data silently changes under it: a bug that's hard to trace. Treat props as read-only.
  • Should you copy a prop into state? Usually not. Compute what you need from props during render; copying creates a second value that stops updating when the prop changes.

02Rendering

0/6 got it

Q05Not marked yetFresherAsked very oftenPredict the output

A click handler calls setCount(count + 1) three times and then logs count. What is logged, what does the screen show, and how would you make it add 3?

Say this first~30 sec to say

The log shows 0 and the screen shows 1. During this render, count is a fixed snapshot of 0, so all three calls ask React to set count to 0 + 1. React batches the updates and re-renders once, with 1; the console.log runs before that re-render, so it sees the old snapshot. To add 3, use the updater form, setCount(c => c + 1), three times: each updater receives the latest pending value.

Could you say it?
Full answer2 min read
JSX
import { useState } from "react";

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

  function handleClick() {
    setCount(count + 1);    // set to 0 + 1
    setCount(count + 1);    // set to 0 + 1 again
    setCount(count + 1);    // and again
    console.log(count);     // 0: this render's snapshot
  }

  return <button onClick={handleClick}>{count}</button>;   // shows 1 after one click
}

The fix

JSX
function handleClick() {
  setCount((c) => c + 1);   // 0 → 1
  setCount((c) => c + 1);   // 1 → 2
  setCount((c) => c + 1);   // 2 → 3
}

How React processes the queue

CallQueued asResult after the click
setCount(count + 1) × 3"replace with 1", three times1
setCount(c => c + 1) × 3"add 1 to whatever it is", three times3

Batching everywhere: since React 18, updates are batched inside timeouts, promises and native event handlers too, not just React event handlers. Older material says batching only happens in event handlers.

Follow-ups

  • How do you use the new value right after setting it? Compute it first: const next = count + 1; setCount(next); save(next);.
  • Why does React work like this? Batching avoids several re-renders for one interaction, and snapshots keep each render internally consistent: everything in it sees the same values.
Q06Not marked yetFresherAsked very oftenPredict the output

A list of text inputs uses key={index}. You type into the first input, then delete the first item. What do the remaining inputs show, and why?

Say this first~30 sec to say

The text you typed moves to the wrong row: it now sits next to the item that moved up. React matches list items by key. With index keys, after deleting item 0, the item that moves into position 0 gets key 0, so React reuses the first row's DOM node, and the text typed into it, for that item, and removes the last row instead. Stable, unique keys such as database IDs keep each row's state with the right item.

Could you say it?
Full answer3 min read
JSX
import { useState } from "react";

const initial = [
  { id: "a1", name: "Asha" },
  { id: "r2", name: "Ravi" },
  { id: "p3", name: "Priya" },
];

export default function Guests() {
  const [guests, setGuests] = useState(initial);
  const remove = (id) => setGuests(guests.filter((g) => g.id !== id));

  return (
    <ul>
      {guests.map((guest, index) => (
        <li key={index}>                         {/* the bug: try key={guest.id} */}
          {guest.name} <input aria-label={`Note for ${guest.name}`} />
          <button onClick={() => remove(guest.id)}>Remove</button>
        </li>
      ))}
    </ul>
  );
}

Type "vegetarian" into Asha's note, then remove Asha:

KeyRavi's row showsPriya's row shows
key={index}"vegetarian": Asha's old inputEmpty
key={guest.id}EmptyEmpty

With index keys, React sees keys 0, 1 and 2 become keys 0 and 1, concludes that the last item was removed, and keeps the first two DOM nodes, including the uncontrolled input holding "vegetarian", just with new names.

Rules for keys

  • Unique among siblings, and stable: the same item must get the same key on every render.
  • Take keys from the data (an ID), not from the position.
  • key={index} is acceptable only when the list never reorders, never has items inserted or removed except at the end, and its items hold no state.
  • Never key={Math.random()}: every render gives every row a new key, so React rebuilds them all, losing focus and state.

Follow-ups

  • Why do lists need keys at all? Without them, React can only match items by position, which is exactly the index-key problem. Keys let it match items by identity when they move.
  • Do keys need to be globally unique? No, only among siblings in the same list.
Q07Not marked yetFresherAsked very oftenCoding

Build a todo list where you can add items, mark them as done and delete them

Say this first~25 sec to say

Keep the todos in state as an array of objects like { id, text, done }. Add by creating a new array with the item appended; toggle by mapping over the array and copying the matching item with done flipped; delete by filtering it out. Never mutate in place: React compares references to decide whether to re-render. Render with map, using each todo's id as its key.

Could you say it?
Full answer3 min read
JSX
import { useState } from "react";

export default function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [text, setText] = useState("");

  function addTodo(event) {
    event.preventDefault();                       // stop the form reloading the page
    const trimmed = text.trim();
    if (!trimmed) return;
    setTodos([...todos, { id: crypto.randomUUID(), text: trimmed, done: false }]);
    setText("");
  }

  function toggleTodo(id) {
    setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
  }

  function deleteTodo(id) {
    setTodos(todos.filter((t) => t.id !== id));
  }

  return (
    <div>
      <form onSubmit={addTodo}>
        <input aria-label="New todo" value={text} onChange={(e) => setText(e.target.value)} />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <label style={{ textDecoration: todo.done ? "line-through" : "none" }}>
              <input type="checkbox" checked={todo.done} onChange={() => toggleTodo(todo.id)} />
              {todo.text}
            </label>
            <button onClick={() => deleteTodo(todo.id)} aria-label={`Delete ${todo.text}`}>
              Delete
            </button>
          </li>
        ))}
      </ul>
      <p>{todos.filter((t) => !t.done).length} left</p>
    </div>
  );
}

What interviewers check

  • Immutable updates: spread to add, map to change one item, filter to remove. todos.push(...) followed by setTodos(todos) wouldn't re-render (question 14).
  • Stable keys: crypto.randomUUID() gives each todo an id once, when it's created; the index would break with deletion (question 6).
  • A form, not just a button: pressing Enter submits, and preventDefault() stops the page reloading.
  • Derived data: the "left" count is computed during render, not kept in separate state.

Follow-ups

  • How would you keep the todos after a page refresh? Save them in an effect, useEffect(() => localStorage.setItem("todos", JSON.stringify(todos)), [todos]), and read them back in a lazy initial state: useState(() => JSON.parse(localStorage.getItem("todos") ?? "[]")).
  • When would you switch to useReducer? When editing, filtering and clearing completed items join the list of actions (question 20).
Q08Not marked yetFresherAsked oftenConcept

What causes a React component to re-render?

Say this first~30 sec to say

A component re-renders when its own state changes (a setter called with a different value), when its parent re-renders, or when a context it reads changes. The common miss: a child re-renders whenever its parent does, even if its props didn't change, unless it's wrapped in React.memo. Props changing isn't a separate trigger; props only change because the parent re-rendered. And re-rendering means calling the function again; React then touches only the DOM that changed.

Could you say it?
Full answer2 min read
JSX
import { useState } from "react";

let childRenders = 0;

function Child() {
  childRenders += 1;
  return <p>Child rendered {childRenders} times</p>;
}

export default function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Clicked {count}</button>
      <Child />                       {/* no props at all, yet it re-renders on every click */}
    </>
  );
}

After three clicks, Child has rendered four times: once on mount, then once per parent update, even though nothing it receives has changed.

TriggerExample
Its state changessetOpen(true) when open was false
Its parent re-rendersAny state change higher up the tree
A context it reads changesuseContext(ThemeContext) and the theme switches
Not a triggerSetting state to the same value (Object.is equal); changing a ref; mutating an object in place

Is re-rendering a problem? Usually not. Rendering is calling a function and diffing its output; React only changes the DOM where something is different. Optimise with React.memo, useMemo and useCallback only after the React DevTools Profiler shows a real cost (question 29).

Follow-ups

  • How do you stop a child re-rendering? Wrap it in React.memo and keep its props referentially stable, or move the changing state down into a smaller component so fewer components sit under it.
  • Why might a component render twice in development? StrictMode deliberately double-invokes rendering to surface impure components. Production renders once.
Q09Not marked yetFresherAsked oftenCoding

Show a list of products with a search box that filters the list as the user types

Say this first~25 sec to say

Store only the search text in state, with a controlled input. Compute the filtered list during render: products.filter(p => p.name.toLowerCase().includes(query.toLowerCase())). Don't keep a second, filtered copy in state, because it duplicates the source of truth and can go stale. If filtering is expensive, wrap it in useMemo; if each search calls an API, debounce the input (question 21).

Could you say it?
Full answer3 min read
JSX
import { useState } from "react";

const products = [
  { id: 1, name: "Steel Water Bottle", price: 399 },
  { id: 2, name: "Electric Kettle", price: 1299 },
  { id: 3, name: "Water Purifier", price: 8999 },
  { id: 4, name: "Notebook", price: 89 },
];

export default function ProductSearch() {
  const [query, setQuery] = useState("");

  const q = query.trim().toLowerCase();
  const visible = products.filter((p) => p.name.toLowerCase().includes(q));   // derived, not stored

  return (
    <div>
      <input
        type="search"
        aria-label="Search products"
        placeholder="Search products"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <ul>
        {visible.map((p) => (
          <li key={p.id}>{p.name} ₹{p.price}</li>
        ))}
      </ul>
      {visible.length === 0 && <p>No products match “{query}”.</p>}
    </div>
  );
}

Typing "water" shows the bottle and the purifier. Clearing the box shows all four again, because the full list was never changed.

The anti-pattern: syncing a copy with an effect

JSX
const [visible, setVisible] = useState(products);
useEffect(() => {
  setVisible(products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())));
}, [query]);

This renders twice per keystroke (once with the stale list, then again after the effect) and adds state that can drift out of sync. If a value can be computed from props and state, compute it.

Follow-ups

  • When would you add useMemo? When the list is long or the filter is costly, and profiling shows it matters: useMemo(() => products.filter(...), [q]).
  • How would you search on the server instead? Debounce the query (question 21), fetch in an effect, and ignore responses that arrive out of order (question 19).
Q10Not marked yetFresherAsked sometimesPredict the output

What does {cart.length && <CartSummary />} render when the cart is empty, and how would you fix it?

Say this first~25 sec to say

It renders the number 0 on the page. && returns its left side when that side is falsy, so the expression evaluates to 0, and React renders numbers. It skips only false, null, undefined and true. Make the left side a real boolean, cart.length > 0 && <CartSummary />, or use a ternary: cart.length ? <CartSummary /> : null.

Could you say it?
Full answer2 min read
JSX
function CartSummary() {
  return <p>Your cart</p>;
}

export function Buggy({ cart }) {
  return <div>{cart.length && <CartSummary />}</div>;       // empty cart: shows "0"
}

export function Fixed({ cart }) {
  return <div>{cart.length > 0 && <CartSummary />}</div>;   // empty cart: shows nothing
}

Ways to render conditionally

PatternUse when
if (!user) return <Login />; at the topWhole different output for a case
cond ? <A /> : <B />Choosing between two things
cond && <A />Show something or nothing, with a boolean cond
return nullRender nothing at all

What React renders for each value

Value in JSXRenders
"text", 42, 0The text or number
false, null, undefined, trueNothing
NaN"NaN": the same trap as 0
An array of elementsEach element

Follow-ups

  • Why does React render 0 but not false? Numbers are legitimate content (a count of zero is real information); booleans are almost always the result of a condition.
  • Can a linter catch it? Yes: ESLint's react/jsx-no-leaked-render rule flags non-boolean values on the left of &&.

03Core hooks

0/5 got it

Q11Not marked yetFresherAsked very oftenConcept

How does useEffect cover what componentDidMount, componentDidUpdate and componentWillUnmount did in class components? Explain no dependency array vs [] vs [value], and when the cleanup runs

Say this first~30 sec to say

An effect runs after React has rendered and updated the screen. With no dependency array, it runs after every render. With [], once after the first render, like componentDidMount. With [value], after the first render and whenever value changes, like componentDidUpdate for that value. The function it returns is the cleanup: it runs before the effect runs again, and on unmount, like componentWillUnmount. In development, StrictMode runs effects twice on mount to expose missing cleanups.

Could you say it?
Full answer3 min read
JSX
import { useEffect, useState } from "react";

function OnlineStatus({ userId }) {
  const [online, setOnline] = useState(false);

  useEffect(() => {
    const connection = subscribeToStatus(userId, setOnline);   // set up
    return () => connection.unsubscribe();                     // clean up
  }, [userId]);           // re-run only when userId changes

  return <p>{online ? "Online" : "Offline"}</p>;
}

When userId changes from 7 to 8, React first runs the cleanup for user 7, then the effect for user 8, so the component is never subscribed to two users at once.

Dependency arrayRuns afterClass equivalent
None: useEffect(fn)Every rendercomponentDidMount + every componentDidUpdate
[]The first render onlycomponentDidMount
[userId]The first render, and when userId changescomponentDidUpdate with an if (prev.userId !== this.props.userId) check
Cleanup (the returned function)Before the next run, and on unmountcomponentWillUnmount

The same component as a class, for comparison

JSX
class OnlineStatusClass extends React.Component {
  state = { online: false };
  componentDidMount() {
    this.connection = subscribeToStatus(this.props.userId, (online) => this.setState({ online }));
  }
  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.connection.unsubscribe();
      this.connection = subscribeToStatus(this.props.userId, (online) => this.setState({ online }));
    }
  }
  componentWillUnmount() {
    this.connection.unsubscribe();
  }
  render() {
    return <p>{this.state.online ? "Online" : "Offline"}</p>;
  }
}

One effect replaced three methods, and the setup and its cleanup sit next to each other. Class components still work in React 19, but you'll mostly meet them in older code and in this comparison.

Why your effect runs twice in development: since React 18, <StrictMode> mounts each component, unmounts it and mounts it again, in development only. If an effect without a proper cleanup misbehaves when run twice (two subscriptions, two analytics events), the double run exposes it before users do.

Follow-ups

  • What shouldn't go in an effect? Values you can compute during render, such as filtered lists or totals (question 9), and logic that belongs in an event handler, such as sending a form when Submit is clicked.
  • Why must every value the effect uses be in the dependency array? Otherwise the effect keeps reading the values from the render it was created in: a stale closure (question 15). The react-hooks/exhaustive-deps lint rule checks this.
Q12Not marked yetFresherAsked very oftenCoding

Build a counter with increment, decrement and reset buttons where the count never drops below zero

Say this first~25 sec to say

Keep the count in useState(0). Increment with the updater form, setCount(c => c + 1), so quick repeated clicks never build on a stale value. For decrement, guard the lower limit, setCount(c => Math.max(0, c - 1)), and disable the button at zero so the limit is visible to the user. Reset sets it straight back to 0.

Could you say it?
Full answer2 min read
JSX
import { useState } from "react";

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

  return (
    <div>
      <p aria-live="polite">Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <button onClick={() => setCount((c) => Math.max(0, c - 1))} disabled={count === 0}>
        Decrement
      </button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

Why both the guard and disabled? disabled is the user-facing rule; Math.max is the safety net, in case the decrement is triggered another way, such as a keyboard shortcut added later.

Common follow-up: a configurable step and maximum

JSX
export function StepCounter({ step = 1, max = 10 }) {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => Math.min(max, c + step))} disabled={count >= max}>+{step}</button>
      <button onClick={() => setCount((c) => Math.max(0, c - step))} disabled={count === 0}>-{step}</button>
    </div>
  );
}

Follow-ups

  • When does setCount(count + 1) go wrong? When several updates happen before a re-render, such as two calls in one handler, because both read the same snapshot (question 5).
  • How would you test it? Render it with React Testing Library, click Decrement at zero, and assert that the text still says "Count: 0" and the button is disabled.
Q13Not marked yetFresherAsked oftenConcept

What is useRef used for, and why doesn't changing ref.current re-render the component?

Say this first~30 sec to say

useRef returns a mutable object, { current: value }, that React keeps the same across renders. Changing ref.current doesn't re-render, because a ref isn't state: React doesn't watch it. Use refs for values the screen doesn't show: DOM nodes (to focus or measure an element), timer IDs, the previous value of a prop, anything that must survive re-renders without causing them. If the screen should display it, use state.

Could you say it?
Full answer3 min read

Reaching a DOM node

JSX
import { useRef } from "react";

export default function SearchBar() {
  const inputRef = useRef(null);
  return (
    <>
      <input ref={inputRef} aria-label="Search" />
      <button onClick={() => inputRef.current.focus()}>Focus search</button>
    </>
  );
}

Keeping a value without re-rendering

JSX
import { useRef, useState } from "react";

export function ClickTracker() {
  const clicks = useRef(0);                    // survives re-renders, never causes one
  const [shown, setShown] = useState(0);

  return (
    <>
      <button onClick={() => { clicks.current += 1; }}>Click me</button>
      <button onClick={() => setShown(clicks.current)}>Show count</button>
      <p>Clicks so far: {shown}</p>
    </>
  );
}

Clicking "Click me" five times changes nothing on screen. Pressing "Show count" copies the ref into state, and the screen updates to 5.

React 19: ref is a normal prop

JSX
function TextInput({ ref, ...props }) {          // React 19: no forwardRef needed
  return <input ref={ref} {...props} />;
}

Before React 19, passing a ref through your own component needed forwardRef. It still works, but new code doesn't need it.

useRefuseState
Changing it re-rendersNoYes
Read during renderAvoid: it may not match the screenYes
Good forDOM nodes, timer IDs, previous values, instance-like dataAnything the UI displays

Follow-ups

  • How do you keep the previous value of a prop? Store it in a ref in an effect, useEffect(() => { prev.current = value; }), so during the next render prev.current still holds the old one.
  • Why not use a plain variable outside the component? It would be shared by every instance of the component. A ref belongs to one instance.
Q14Not marked yetFresherAsked oftenPredict the output

A handler runs items.push(newItem) and then setItems(items). Does the list update on screen, and why?

Say this first~25 sec to say

No. push mutates the existing array, so setItems receives the same array reference React already has. React compares old and new state with Object.is, sees the same object, and skips the re-render. Create a new array instead: setItems([...items, newItem]), or setItems(prev => [...prev, newItem]). The same rule applies to objects: spread into a new object rather than assigning a property.

Could you say it?
Full answer2 min read
JSX
import { useState } from "react";

export default function Shopping() {
  const [items, setItems] = useState(["Milk"]);

  function addBroken() {
    items.push("Bread");          // mutates the array React is holding
    setItems(items);              // same reference: React skips the update
  }

  function addFixed() {
    setItems([...items, "Eggs"]); // a new array: React re-renders
  }

  return (
    <>
      <button onClick={addBroken}>Add bread (broken)</button>
      <button onClick={addFixed}>Add eggs</button>
      <ul>{items.map((item, i) => <li key={i}>{item}</li>)}</ul>
    </>
  );
}

Click "Add bread (broken)": still only Milk on screen. Click "Add eggs": Milk, Bread and Eggs all appear, because the bread was already sitting in the mutated array, waiting for any re-render. That delayed, surprising appearance is why mutation bugs are hard to trace.

Immutable update patterns

GoalWrite
Add[...items, x] or [x, ...items]
Removeitems.filter((i) => i.id !== id)
Change oneitems.map((i) => (i.id === id ? { ...i, done: true } : i))
Change an object field{ ...user, name: "Ravi" }
Change a nested field{ ...user, address: { ...user.address, city: "Pune" } }
Sort without mutating[...items].sort(...) or items.toSorted(...)

Follow-ups

  • Why does React compare by reference? It's instant, whatever the size of the data. Deep comparison of large objects on every update would be slow.
  • Is there a way to write "mutating" code safely? Immer lets you write draft.items.push(x) and produces a new immutable object. Redux Toolkit uses it internally (question 28).
Q15Not marked yet1–3 yrsAsked oftenPredict the output

An effect with an empty dependency array starts setInterval(() => setCount(count + 1), 1000). What does the counter show after five seconds, and why?

Say this first~25 sec to say

It shows 1, and stays at 1. The effect ran once, after the first render, so its interval callback closed over count from that render, which is 0. Every tick calls setCount(0 + 1). That's a stale closure. Fix it with the updater form, setCount(c => c + 1), which always receives the latest value, and clear the interval in the effect's cleanup.

Could you say it?
Full answer2 min read
JSX
import { useEffect, useState } from "react";

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

  useEffect(() => {
    const id = setInterval(() => setCount(count + 1), 1000);   // count is always 0 in here
    return () => clearInterval(id);
  }, []);

  return <p>{count}</p>;     // 1, forever
}

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

  useEffect(() => {
    const id = setInterval(() => setCount((c) => c + 1), 1000);   // no stale value to read
    return () => clearInterval(id);                                // stop on unmount
  }, []);

  return <p>{count}</p>;     // 1, 2, 3, 4, 5...
}

Other fixes, and why the updater is best

  • Adding count to the dependency array also works, but it tears down and recreates the interval every second.
  • Keeping the latest value in a ref works when the callback needs to read the value for something other than setting state.
  • The updater form needs no dependencies at all, so the interval is created once.

Follow-ups

  • What happens without the cleanup? The interval keeps running after the component unmounts. In development, StrictMode's double mount would create two intervals, and the counter would jump by 2.
  • Where else do stale closures appear? Event listeners added in an effect, and callbacks wrapped in useCallback with missing dependencies.

04More hooks

0/6 got it

Q16Not marked yetFresherAsked very oftenConcept

useMemo vs useCallback: what does each one cache, and when is using them worth it?

Say this first~25 sec to say

useMemo caches a computed value: it re-runs the calculation only when a dependency changes. useCallback caches a function, keeping the same function reference between renders until a dependency changes; useCallback(fn, deps) equals useMemo(() => fn, deps). They're worth it for genuinely expensive calculations, or when a stable reference matters, such as a prop for a React.memo child or another hook's dependency. Elsewhere they only add complexity.

Could you say it?
Full answer3 min read
JSX
import { memo, useCallback, useMemo, useState } from "react";

const ProductRow = memo(function ProductRow({ product, onAdd }) {
  return (
    <li>
      {product.name} <button onClick={() => onAdd(product.id)}>Add</button>
    </li>
  );
});

export default function Catalogue({ products }) {
  const [query, setQuery] = useState("");
  const [cart, setCart] = useState([]);

  // useMemo: re-filter only when the products or the query change
  const visible = useMemo(
    () => products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())),
    [products, query]
  );

  // useCallback: the same function on every render, so memo'd rows don't re-render
  const handleAdd = useCallback((id) => setCart((c) => [...c, id]), []);

  return (
    <>
      <input aria-label="Filter" value={query} onChange={(e) => setQuery(e.target.value)} />
      <p>{cart.length} in cart</p>
      <ul>
        {visible.map((p) => (
          <ProductRow key={p.id} product={p} onAdd={handleAdd} />
        ))}
      </ul>
    </>
  );
}

Adding to the cart re-renders Catalogue, but every ProductRow keeps the same product object and the same handleAdd function, so memo lets all of them skip rendering.

useMemouseCallback
CachesThe result of a functionThe function itself
Typical useFiltering or sorting a big list, heavy mathsCallbacks passed to memo children or used in effect dependencies
EquivalentuseMemo(() => fn, deps)

When not to bother: cheap calculations, and callbacks passed to ordinary elements such as <button>. Memoisation itself costs memory and a dependency comparison on every render.

The React Compiler: an opt-in build-time tool, separate from React 19 itself, that adds this memoisation automatically. You still need to explain the hooks: plenty of codebases don't use the compiler, and interviewers ask anyway.

Follow-ups

  • Does useMemo guarantee the value is never recomputed? No. React may discard the cache, for example for offscreen components, so treat it as a performance hint, not a guarantee.
  • What breaks memoisation most often? Inline objects and arrays in the dependency list, such as [{ id }], which are new on every render.
Q17Not marked yetFresherAsked very oftenConcept

What is prop drilling, and how do createContext and useContext avoid it?

Say this first~30 sec to say

Prop drilling is passing a prop through several components that don't use it, just so a deeply nested one can. Context fixes it: createContext makes a context, a provider near the top supplies a value, and any component below reads it with useContext, however deep it is. In React 19 you render the context itself as the provider, <ThemeContext value="dark">; older code writes <ThemeContext.Provider value="dark">. Every component that reads a context re-renders when its value changes.

Could you say it?
Full answer2 min read
JSX
import { createContext, useContext, useState } from "react";

const ThemeContext = createContext("light");          // the default, used with no provider above

function ThemeLabel() {
  const theme = useContext(ThemeContext);              // read it, however deep
  return <p>Current theme: {theme}</p>;
}

function Sidebar() {
  return <ThemeLabel />;                               // no theme prop passed through here
}

export default function App() {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext value={theme}>                       {/* React 19: the context is the provider */}
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>Switch theme</button>
      <Sidebar />
    </ThemeContext>
  );
}

Without context, App would pass theme to Sidebar only so that Sidebar could pass it to ThemeLabel. With context, Sidebar doesn't know or care.

Good uses: the theme, the signed-in user, the language, feature flags: values that many components need and that change rarely.

Watch out for:

  • Re-renders: when the value changes, every component that reads it re-renders. Passing a new object each render, value={{ user, logout }}, re-renders them all on every parent render; wrap it in useMemo.
  • Overuse: if only two levels separate the components, passing a prop is clearer. Composition, passing components as children, often removes drilling without context.

Follow-ups

  • Is context a state manager? No. It's a way to pass a value down; the state still lives in a useState or useReducer somewhere above. See question 27 for when to reach for Redux.
  • What does the default value in createContext do? It's used only when a component has no matching provider above it, which is handy in tests.
Q18Not marked yetFresherAsked very oftenConcept

What are the rules of hooks, and why can't a hook be called inside a condition or a loop?

Say this first~30 sec to say

Hooks are use… functions that give components state, effects and context. Rule one: call them only at the top level of a component or custom hook, never in conditions, loops, nested functions or after an early return. Rule two: call them only from components or custom hooks. React matches each hook to its saved state by call order, so if a call is skipped, every later hook reads the wrong slot.

Could you say it?
Full answer2 min read
JSX
import { useState } from "react";

function Profile({ user }) {
  const [tab, setTab] = useState("posts");
  if (user) {
    const [following, setFollowing] = useState(false);   // inside a condition: breaks the rule
  }
  const [likes, setLikes] = useState(0);                 // its slot now depends on user
  return <h1>{user ? user.name : "Guest"}: {likes} likes</h1>;
}

function ProfileFixed({ user }) {
  const [tab, setTab] = useState("posts");
  const [following, setFollowing] = useState(false);     // always called, used only when needed
  const [likes, setLikes] = useState(0);
  return <h1>{user ? user.name : "Guest"}: {likes} likes</h1>;
}

When Profile first renders without a user and then with one, the number and order of its hooks change. In development, React logs "React has detected a change in the order of Hooks called by Profile" and throws "Rendered more hooks than during the previous render."

An early return placed before a hook breaks the same rule, even though React doesn't always complain straight away. Put every hook first, then any early returns.

How React tracks hooks (simplified)

No user:    useState("posts") → slot 0    useState(0) → slot 1
With user:  useState("posts") → slot 0    useState(false) → slot 1    ← React expected likes here
                                           useState(0) → slot 2        ← a slot that never existed

The likes hook moved from slot 1 to slot 2, so React can no longer match hooks to their saved state.

The exception: React 19's use()

use(promiseOrContext) reads a promise or a context, and unlike other hooks it can be called inside conditions and loops.

Enforcing the rules: the eslint-plugin-react-hooks package (rules-of-hooks, exhaustive-deps) catches violations as you type. Every React project should have it on.

Follow-ups

  • Why must custom hooks start with use? So the linter, and readers, know the rules of hooks apply inside them.
  • Can you call a hook inside useEffect? No: that's a nested function. Call the hook at the top level and use its result inside the effect.
Q19Not marked yetFresherAsked oftenCoding

Write a custom hook useFetch(url) that returns data, loading and error

Say this first~30 sec to say

A custom hook is a function whose name starts with use and which calls other hooks, so components can share stateful logic. useFetch keeps data, loading and error in state and starts the request in a useEffect that depends on url. Check response.ok, since fetch only rejects on network failures. In the cleanup, abort the request with an AbortController, so a slow old response can't overwrite a newer one after url changes or the component unmounts.

Could you say it?
Full answer3 min read
JSX
import { useEffect, useState } from "react";

export function useFetch(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null });

  useEffect(() => {
    const controller = new AbortController();
    setState({ data: null, loading: true, error: null });

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);   // 404 and 500 don't reject on their own
        return res.json();
      })
      .then((data) => setState({ data, loading: false, error: null }))
      .catch((error) => {
        if (error.name !== "AbortError") setState({ data: null, loading: false, error });
      });

    return () => controller.abort();    // cancel when url changes or the component unmounts
  }, [url]);

  return state;
}

export function UserCard({ id }) {
  const { data, loading, error } = useFetch(`/api/users/${id}`);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Could not load the user ({error.message}).</p>;
  return <p>{data.name}</p>;
}

Why each part is there

  • [url] dependency: a new URL starts a new request.
  • res.ok check: fetch resolves even for a 404 or 500; only network failures reject.
  • AbortController: if the user clicks from user 1 to user 2 quickly, user 1's slow response would otherwise arrive last and overwrite user 2's. Aborting the old request prevents that race.
  • Ignoring AbortError: the abort itself rejects the promise, which isn't a real error.

Follow-ups

  • Why not make the effect function async? An effect must return a cleanup function or nothing; an async function returns a promise. Call an async function from inside the effect instead.
  • Would you write this in production? Usually not: TanStack Query or SWR add caching, deduplication of identical requests, retries and background refetching. But writing useFetch is how interviewers check you understand effects and races.
Q20Not marked yet1–3 yrsAsked oftenConcept

When would you choose useReducer over useState?

Say this first~25 sec to say

Reach for useReducer when state has several related fields that change together, many kinds of update, or rules where the next state depends on the previous one. A reducer is a pure function, (state, action) => newState, that keeps every update rule in one place. Components just dispatch({ type: "added", item }), and the reducer can be unit-tested without rendering anything. For one independent value, useState is simpler.

Could you say it?
Full answer3 min read
JSX
import { useReducer } from "react";

const initialCart = { items: [], couponApplied: false };

export function cartReducer(state, action) {
  switch (action.type) {
    case "added":
      return { ...state, items: [...state.items, action.item] };
    case "removed":
      return { ...state, items: state.items.filter((i) => i.id !== action.id) };
    case "couponApplied":
      return { ...state, couponApplied: true };
    case "cleared":
      return initialCart;
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

export default function Cart() {
  const [cart, dispatch] = useReducer(cartReducer, initialCart);
  const total = cart.items.reduce((sum, i) => sum + i.price, 0) * (cart.couponApplied ? 0.9 : 1);

  return (
    <>
      <button onClick={() => dispatch({ type: "added", item: { id: Date.now(), price: 500 } })}>Add item</button>
      <button onClick={() => dispatch({ type: "couponApplied" })}>Apply 10% coupon</button>
      <button onClick={() => dispatch({ type: "cleared" })}>Clear</button>
      <p>{cart.items.length} items, total ₹{total}</p>
    </>
  );
}

Testing the logic without React

JavaScript
let state = cartReducer(initialCart, { type: "added", item: { id: 1, price: 500 } });
state = cartReducer(state, { type: "couponApplied" });
console.log(state.items.length, state.couponApplied);   // 1 true
Choose useState whenChoose useReducer when
One or two independent valuesSeveral fields that change together
Updates are simple assignmentsThere are many named kinds of update
The logic is shortYou want the update rules in one testable place
The next state depends on the previous state in non-trivial ways

Follow-ups

  • How does this relate to Redux? It's the same reducer idea, but local to one component. Redux puts one reducer-managed store at the top of the whole app (question 28).
  • Can a reducer call an API? No. Reducers must be pure: no side effects, no randomness. Do the fetch in an event handler or effect, then dispatch the result.
Q21Not marked yet1–3 yrsAsked oftenCoding

Write a useDebounce(value, delay) hook and use it to delay a search request until the user stops typing

Say this first~30 sec to say

Keep the debounced value in state. In a useEffect that depends on value and delay, start a setTimeout that copies value into the debounced state after delay, and return a cleanup that clears that timeout. Every keystroke re-runs the effect, and its cleanup cancels the previous timer, so only a value that stays unchanged for the whole delay gets through. Then make the search effect depend on the debounced value, not the raw one.

Could you say it?
Full answer3 min read
JSX
import { useEffect, useState } from "react";

export function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);       // a new keystroke cancels the pending update
  }, [value, delay]);

  return debounced;
}

export function SearchPage({ search }) {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 400);
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (!debouncedQuery) return;
    let ignore = false;
    search(debouncedQuery).then((found) => {
      if (!ignore) setResults(found);    // drop answers to older queries
    });
    return () => { ignore = true; };
  }, [debouncedQuery, search]);

  return (
    <>
      <input aria-label="Search" value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>{results.map((r) => <li key={r}>{r}</li>)}</ul>
    </>
  );
}

Typing "kettle" quickly fires six keystrokes but one search, 400 ms after the last one. The input itself stays instant, because it uses the raw query.

Why a hook, not a plain debounce function? A plain debounce(fn) created inside a component is recreated on every render, so its timer is lost each time; you'd need useMemo or useRef to keep it. The hook version works with React's model instead: the effect's cleanup is the cancellation.

Follow-ups

  • Debounce or throttle? Debounce waits for a pause (search boxes, resizing). Throttle runs at most once per interval (scroll position, button mashing). The Frontend kit's question 20 covers both in plain JavaScript.
  • What does the ignore flag add? Debouncing reduces requests, but responses can still arrive out of order. The flag makes sure only the latest query's results are shown.

05Component patterns

0/5 got it

Q22Not marked yetFresherAsked very oftenConcept

Controlled vs uncontrolled components: what's the difference, and when would you use each?

Say this first~30 sec to say

In a controlled component, React state holds the input's value: you pass value and update it in onChange, so you can validate or reformat as the user types. In an uncontrolled one, the DOM keeps the value: you set only defaultValue and read it when needed, through a ref or the form's data. Use controlled inputs for live validation and dependent fields, uncontrolled ones for simple forms and file inputs.

Could you say it?
Full answer3 min read
JSX
import { useRef, useState } from "react";

export function ControlledPin() {
  const [pin, setPin] = useState("");
  return (
    <input
      aria-label="PIN"
      value={pin}
      onChange={(e) => setPin(e.target.value.replace(/\D/g, "").slice(0, 6))}   // digits only, max 6
    />
  );
}

export function UncontrolledName({ onSave }) {
  const nameRef = useRef(null);
  function handleSubmit(event) {
    event.preventDefault();
    onSave(nameRef.current.value);        // read from the DOM only when needed
  }
  return (
    <form onSubmit={handleSubmit}>
      <input aria-label="Name" ref={nameRef} defaultValue="Asha" />
      <button type="submit">Save</button>
    </form>
  );
}

Typing "12a34567" into the controlled PIN field shows "123456": the state, not the keyboard, decides what the input holds.

ControlledUncontrolled
Value lives inReact stateThe DOM
You setvalue + onChangedefaultValue (or nothing)
Read the valueAny time, from stateThrough a ref, or FormData on submit
Live validation and formattingEasyAwkward
Re-renders per keystrokeYesNo
Good forSearch boxes, dependent fields, instant feedbackSimple forms, <input type="file">, non-React widgets

Two classic warnings

  • Setting value without onChange makes the field read-only, and React warns about it.
  • Switching an input between value={undefined} and a real value makes it jump from uncontrolled to controlled; initialise state with "", not undefined.

Follow-ups

  • Why is a file input always uncontrolled? Its value can only be set by the user, for security, so you read the chosen files from the element.
  • What do form libraries use? React Hook Form is uncontrolled by default, for performance; Formik is controlled.
Q23Not marked yetFresherAsked oftenCoding

Build a sign-up form with email and password fields that shows validation errors and keeps Submit disabled until the form is valid

Say this first~30 sec to say

Keep email and password in state as controlled inputs. Derive the validation errors from the current values during render, rather than storing them separately, and disable Submit while any error exists. Show an error only after its field has been touched, so users aren't scolded before typing. In the submit handler, call event.preventDefault() to stop the page reloading, then send the data. React 19's form Actions are an alternative to onSubmit.

Could you say it?
Full answer4 min read
JSX
import { useState } from "react";

function validate({ email, password }) {
  const errors = {};
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errors.email = "Enter a valid email address.";
  if (password.length < 8) errors.password = "Use at least 8 characters.";
  else if (!/\d/.test(password)) errors.password = "Include at least one number.";
  return errors;
}

export default function SignUpForm({ onSignUp }) {
  const [values, setValues] = useState({ email: "", password: "" });
  const [touched, setTouched] = useState({});

  const errors = validate(values);                       // derived on every render
  const isValid = Object.keys(errors).length === 0;

  const handleChange = (e) => setValues({ ...values, [e.target.name]: e.target.value });
  const handleBlur = (e) => setTouched({ ...touched, [e.target.name]: true });

  function handleSubmit(event) {
    event.preventDefault();                              // no page reload
    if (isValid) onSignUp(values);
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <label>
        Email
        <input name="email" type="email" value={values.email} onChange={handleChange} onBlur={handleBlur} />
      </label>
      {touched.email && errors.email && <p role="alert">{errors.email}</p>}

      <label>
        Password
        <input name="password" type="password" value={values.password} onChange={handleChange} onBlur={handleBlur} />
      </label>
      {touched.password && errors.password && <p role="alert">{errors.password}</p>}

      <button type="submit" disabled={!isValid}>Sign up</button>
    </form>
  );
}

What makes this a good answer

  • One handleChange for every field, using the input's name.
  • Errors are computed, not stored, so they can never disagree with the values.
  • "Touched" tracking means errors appear after the user leaves a field, not on the first keystroke.
  • noValidate turns off the browser's own validation bubbles, so the messages are consistent.
  • The server must validate again: client-side checks are for convenience, not security.

The React 19 alternative: form Actions

JSX
import { useActionState } from "react";

export function NewsletterForm({ subscribe }) {
  const [message, formAction, isPending] = useActionState(async (previous, formData) => {
    const email = formData.get("email");
    if (!email.includes("@")) return "Enter a valid email address.";
    await subscribe(email);
    return "Subscribed!";
  }, "");

  return (
    <form action={formAction}>
      <input name="email" aria-label="Email" />
      <button type="submit" disabled={isPending}>Subscribe</button>
      {message && <p>{message}</p>}
    </form>
  );
}

Passing a function to action lets React handle the submission: it collects the FormData, tracks the pending state and stores the returned result, with no preventDefault needed.

Follow-ups

  • How would you show a server error, such as "email already registered"? Keep a serverError in state, set it when the request fails, and clear it when the user edits the field.
  • Would you use a library? For large forms, React Hook Form with a schema validator such as Zod saves a lot of code; for an interview, write it by hand.
Q24Not marked yetFresherAsked oftenConcept

What does lifting state up mean, and when do two sibling components need it?

Say this first~30 sec to say

When two siblings need the same changing data, move that state up to their closest common parent. The parent owns it and passes the value down as props, plus a callback to change it, so both children stay in sync from one source of truth. The classic example is a Celsius input and a Fahrenheit input: if each kept its own state, they'd disagree as soon as you typed into one.

Could you say it?
Full answer2 min read
JSX
import { useState } from "react";

function TemperatureInput({ label, value, onChange }) {
  return (
    <label>
      {label}
      <input value={value} onChange={(e) => onChange(e.target.value)} />
    </label>
  );
}

const toF = (c) => (c === "" || isNaN(c) ? "" : String(Math.round((c * 9) / 5 + 32)));
const toC = (f) => (f === "" || isNaN(f) ? "" : String(Math.round(((f - 32) * 5) / 9)));

export default function Converter() {
  const [celsius, setCelsius] = useState("");          // the single source of truth
  return (
    <>
      <TemperatureInput label="Celsius" value={celsius} onChange={setCelsius} />
      <TemperatureInput label="Fahrenheit" value={toF(celsius)} onChange={(f) => setCelsius(toC(f))} />
    </>
  );
}

Typing 100 in Celsius shows 212 in Fahrenheit; typing 32 in Fahrenheit shows 0 in Celsius. The two inputs can't disagree, because only one value exists.

How to decide where state lives

  1. List every component that reads or changes the value.
  2. Find their closest common parent.
  3. Put the state there, or higher, and pass it down.

Lift only as far as needed. State lifted too high makes unrelated components re-render and props travel far, which is when context (question 17) or a store (question 27) becomes worth it.

Follow-ups

  • What's the opposite move? "Colocating" state: moving it down into the one component that uses it, which reduces re-renders.
  • How does a child update lifted state? Through a callback prop, such as onChange; it never sets the parent's state directly.
Q25Not marked yet1–3 yrsAsked oftenConcept

What is a higher-order component, and what has largely replaced it in modern React?

Say this first~30 sec to say

A higher-order component (HOC) is a function that takes a component and returns a new, enhanced one: withLoading(UserList) renders a spinner while loading and the list otherwise. HOCs were the main way to share logic in the class-component days; Redux's connect and React Router's withRouter are well-known examples. Custom hooks have largely replaced them, because they share logic without wrapping components: no wrapper layers, no props colliding, and the data flow is visible inside the component.

Could you say it?
Full answer3 min read

A small HOC

JSX
export function withLoading(Component) {
  function WithLoading({ isLoading, ...props }) {
    if (isLoading) return <p>Loading...</p>;
    return <Component {...props} />;          // pass every other prop through
  }
  WithLoading.displayName = `withLoading(${Component.displayName || Component.name})`;
  return WithLoading;
}

function UserList({ users }) {
  return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

export const UserListWithLoading = withLoading(UserList);
// <UserListWithLoading isLoading={loading} users={users} />

The same sharing with a hook

JSX
import { useEffect, useState } from "react";

export function useUsers(fetchUsers) {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    fetchUsers().then((list) => {
      setUsers(list);
      setLoading(false);
    });
  }, [fetchUsers]);
  return { users, loading };
}

function Team({ fetchUsers }) {
  const { users, loading } = useUsers(fetchUsers);   // the data flow is visible right here
  if (loading) return <p>Loading...</p>;
  return <UserList users={users} />;
}
HOCCustom hook
SharesLogic and a wrapping componentLogic only
Component treeOne extra layer per HOC ("wrapper hell")No extra layers
Where data comes fromHidden in props injected from outsideExplicit, in the component body
Prop name clashesPossible when HOCs are stackedNot possible
Still useful forWrapping rendering: error boundaries, auth guards, analyticsAlmost everything else

Follow-ups

  • Why set displayName? So React DevTools shows withLoading(UserList) instead of an anonymous wrapper.
  • What are render props? Passing a function as a prop that returns UI, <DataLoader render={(data) => ...} />: another pre-hooks pattern for sharing logic.
Q26Not marked yet1–3 yrsAsked sometimesConcept

What is an error boundary, and which errors does it not catch?

Say this first~30 sec to say

An error boundary is a component that catches errors thrown while rendering its children, in their lifecycle methods or constructors, and shows a fallback UI instead of letting the whole app go blank. It must be a class component with static getDerivedStateFromError, plus componentDidCatch for logging; React 19 still has no hook version, so many teams use the react-error-boundary package. It doesn't catch errors in event handlers, async code such as timers and promises, server rendering, or the boundary itself.

Could you say it?
Full answer3 min read
JSX
import { Component } from "react";

export class ErrorBoundary extends Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };                  // render the fallback next
  }

  componentDidCatch(error, info) {
    logToService(error, info.componentStack);   // e.g. Sentry
  }

  render() {
    if (this.state.hasError) return this.props.fallback ?? <p>Something went wrong.</p>;
    return this.props.children;
  }
}

function logToService(error, stack) {
  // send to your monitoring service
}

function Price({ product }) {
  return <p>₹{product.price.toFixed(2)}</p>;    // throws if product is undefined
}

export default function Shop() {
  return (
    <ErrorBoundary fallback={<p>Could not show the price.</p>}>
      <Price product={undefined} />
    </ErrorBoundary>
  );
}

Price throws during render, and the boundary shows "Could not show the price." while the rest of the page keeps working.

What it doesn't catch, and what to do instead

Error thrown inCaught by a boundary?Handle it with
Rendering, lifecycle methods, constructors of childrenYes
Event handlers (onClick)Notry / catch in the handler, then set error state
Async code: setTimeout, promises, fetchNo.catch() or try / catch in async functions, then set error state
Server-side renderingNoThe framework's error handling
The boundary component itselfNoA boundary higher up

A common trick turns an async error into a render error the boundary can catch: catch it, store it in state, and throw it during the next render.

Where to put boundaries: around independent sections, such as each dashboard widget or route, so one failure doesn't take down the rest, plus one at the top as a last resort.

Follow-ups

  • Why isn't there a hook? getDerivedStateFromError has no hooks equivalent yet. React 19 did add onCaughtError and onUncaughtError options to createRoot for central logging.
  • How do you let the user retry? Reset the boundary's state, for example with a "Try again" button, or use react-error-boundary's resetKeys.

06State management

0/2 got it

Q27Not marked yetFresherAsked very oftenConcept

Context API vs Redux: when does each one make sense?

Say this first~30 sec to say

Context passes a value down the tree without prop drilling. It isn't a state manager by itself, and every component reading it re-renders when its value changes, so it suits values that change rarely: theme, signed-in user, language. Redux is a predictable store for app-wide state that changes often or is updated from many places, with pure reducers, middleware for side effects, and DevTools for time-travel debugging. Redux Toolkit removes most of its old boilerplate.

Could you say it?
Full answer2 min read
Context (+ useState or useReducer)Redux (with Redux Toolkit)
What it isA way to pass values down the treeA store, update rules and tooling
Re-rendersEvery consumer, whenever the value changesOnly components whose selected slice changed
DebuggingReact DevToolsRedux DevTools: every action, state diffs, time travel
Side effectsYour own effectsThunks, listener middleware, RTK Query
Set-upBuilt into ReactA package and a store
Best forTheme, auth user, locale, feature flagsLarge, frequently changing shared state; many contributors

A common middle ground

  • Server data (lists, details, anything fetched): a data-fetching library such as TanStack Query or RTK Query, which handles caching and refetching. This is often most of what people once put in Redux.
  • Global UI state (theme, user): context.
  • Complex shared client state (a multi-step builder, an editor): Redux Toolkit, or a lighter store such as Zustand.

How to answer: "Context for values that rarely change; a store when shared state changes often, needs to be traced, or is updated from many places. And server data goes in a data-fetching library either way."

Follow-ups

  • Can context cause performance problems? Yes, when a frequently changing value sits in a context read by many components. Split contexts by how often they change, or memoise the value.
  • Is Redux still worth learning? Yes. Many large codebases use it, and its ideas (actions, reducers, a single source of truth) appear everywhere, including useReducer.
Q28Not marked yetFresherAsked very oftenConcept

How does data flow through Redux (store, action, reducer, dispatch), and what do Redux Toolkit's configureStore and createSlice simplify?

Say this first~30 sec to say

All shared state lives in one store. The UI dispatches an action, a plain object such as { type: "cart/added", payload: item }. The store passes the current state and the action to reducers, pure functions that return the new state. Components subscribed through useSelector re-render with the new data. Redux Toolkit's configureStore sets up the store with good defaults, and createSlice generates the actions and reducer together, letting you write "mutating" code that Immer turns into immutable updates.

Could you say it?
Full answer3 min read
UI ──dispatch(action)──▶ store ──(state, action)──▶ reducer ──new state──▶ store ──useSelector──▶ UI
JavaScript
import { configureStore, createSlice } from "@reduxjs/toolkit";

const cartSlice = createSlice({
  name: "cart",
  initialState: { items: [] },
  reducers: {
    added(state, action) {
      state.items.push(action.payload);        // looks like mutation; Immer makes it immutable
    },
    removed(state, action) {
      state.items = state.items.filter((item) => item.id !== action.payload);
    },
  },
});

export const { added, removed } = cartSlice.actions;   // action creators, generated for you

export const store = configureStore({ reducer: { cart: cartSlice.reducer } });

console.log(added({ id: 3, name: "Kettle" }));
// { type: 'cart/added', payload: { id: 3, name: 'Kettle' } }

store.dispatch(added({ id: 1, name: "Kettle", price: 1299 }));
store.dispatch(added({ id: 2, name: "Mixer", price: 2499 }));
store.dispatch(removed(1));
console.log(store.getState().cart.items);
// [ { id: 2, name: 'Mixer', price: 2499 } ]

Connecting it to React

JSX
import { Provider, useDispatch, useSelector } from "react-redux";

function CartBadge() {
  const count = useSelector((state) => state.cart.items.length);   // re-renders only when this changes
  return <span>Cart ({count})</span>;
}

function AddButton({ product }) {
  const dispatch = useDispatch();
  return <button onClick={() => dispatch(added(product))}>Add to cart</button>;
}

export function App() {
  return (
    <Provider store={store}>
      <CartBadge />
      <AddButton product={{ id: 9, name: "Bottle", price: 399 }} />
    </Provider>
  );
}

What Redux Toolkit simplifies

Classic ReduxRedux Toolkit
Hand-written action type constants and action creatorsGenerated by createSlice
Reducers full of spread operators"Mutating" code, made immutable by Immer
createStore plus manual middleware and DevTools wiringconfigureStore, with thunk, DevTools and mistake checks built in
Separate data-fetching codeRTK Query for API caching

Redux's original createStore is marked deprecated in favour of Redux Toolkit, though older courses still teach it.

Follow-ups

  • Why must reducers be pure? So the same state and action always give the same result. That's what makes time-travel debugging, testing and predictable updates possible.
  • Where do API calls go? In thunks (createAsyncThunk) or RTK Query, never inside a reducer.

07Performance

0/2 got it

Q29Not marked yet1–3 yrsAsked oftenPredict the output

A child wrapped in React.memo gets onSelect={(id) => setSelected(id)} from its parent. Does it re-render when an unrelated counter in the parent changes? Why, and how do you fix it?

Say this first~25 sec to say

Yes, it re-renders every time. React.memo skips a render only if every prop is the same by reference (Object.is). The inline arrow (id) => setSelected(id) is a new function on every parent render, so onSelect always looks changed and memo never helps. Wrap the handler in useCallback, or simply pass setSelected, since state setters keep the same identity across renders.

Could you say it?
Full answer2 min read
JSX
import { memo, useCallback, useState } from "react";

let listRenders = 0;

const CityList = memo(function CityList({ onSelect }) {
  listRenders += 1;
  return ["Pune", "Delhi"].map((city) => (
    <button key={city} onClick={() => onSelect(city)}>{city}</button>
  ));
});

export default function Page() {
  const [count, setCount] = useState(0);
  const [selected, setSelected] = useState(null);

  const handleSelect = useCallback((city) => setSelected(city), []);   // the same function every render

  return (
    <>
      <button onClick={() => setCount(count + 1)}>Unrelated: {count}</button>
      <p>Selected: {selected ?? "none"}</p>
      <CityList onSelect={handleSelect} />          {/* try onSelect={(c) => setSelected(c)} */}
    </>
  );
}
onSelect passed asAfter 5 clicks on "Unrelated", CityList has rendered
(c) => setSelected(c)6 times: memo is defeated
handleSelect from useCallbackOnce
setSelected directlyOnce: setters are stable

Other props that defeat memo: inline objects (style={{ margin: 0 }}), inline arrays (items={[1, 2]}), and new elements as children. Hoist constants out of the component, or memoise them.

Follow-ups

  • What does React.memo's second argument do? It's a custom comparison function, (prevProps, nextProps) => boolean, for when shallow comparison isn't right. Use it sparingly.
  • Should you memo everything? No. Comparing props costs something too. Memo components that render often with the same props, or are expensive to render, after profiling.
Q30Not marked yet1–3 yrsAsked oftenConcept

What is code splitting, and how do React.lazy and Suspense load a component only when it's needed?

Say this first~30 sec to say

Code splitting breaks your JavaScript bundle into smaller chunks that load on demand, so the first page downloads less. A dynamic import() tells the bundler to put a module in its own chunk. React.lazy(() => import("./Reports")) turns that into a component that loads its chunk the first time it renders. Wrap it in <Suspense fallback={...}>, which shows the fallback while the chunk downloads. Splitting by route is the usual first step.

Could you say it?
Full answer2 min read
JSX
import { lazy, Suspense, useState } from "react";

const Reports = lazy(() => import("./Reports"));   // a separate chunk, loaded on first render

export default function Dashboard() {
  const [showReports, setShowReports] = useState(false);
  return (
    <>
      <button onClick={() => setShowReports(true)}>Open reports</button>
      {showReports && (
        <Suspense fallback={<p>Loading reports...</p>}>
          <Reports />
        </Suspense>
      )}
    </>
  );
}

Until "Open reports" is clicked, the Reports code isn't downloaded at all. On the first click, "Loading reports..." shows briefly, then the component appears; later visits use the cached chunk.

Splitting by route

JSX
const Settings = lazy(() => import("./pages/Settings"));
const Billing = lazy(() => import("./pages/Billing"));

<Suspense fallback={<PageSpinner />}>
  <Routes>
    <Route path="/settings" element={<Settings />} />
    <Route path="/billing" element={<Billing />} />
  </Routes>
</Suspense>

Good candidates to split: routes, heavy widgets (charts, rich-text editors, maps), modals and admin panels that most users never open.

Rules and gotchas

  • lazy expects a module with a default export. For a named export: lazy(() => import("./Charts").then((m) => ({ default: m.SalesChart }))).
  • Declare lazy components at the top level of a module, never inside a component, or they're recreated on every render.
  • A failed chunk download throws, so put an error boundary (question 26) around the Suspense.

Follow-ups

  • How do you check it worked? The Network tab shows the new chunk loading on demand, and a bundle analyser shows the main bundle shrinking.
  • What do frameworks do? Next.js splits every page automatically, and offers next/dynamic for components.

08Routing and Next.js basics

0/2 got it

Q31Not marked yetFresherAsked oftenConcept

How does client-side routing work in React Router, and how would you read the id from /users/:id and redirect to another page from code?

Say this first~30 sec to say

Client-side routing maps URLs to components inside one page: clicking a <Link> updates the address bar through the browser's History API and renders the matching route, without a full page reload. Declare routes such as <Route path="/users/:id" element={<UserPage />} />; inside UserPage, useParams() returns { id } from the URL, and useNavigate() returns a function, navigate("/login"), for redirecting from code. That's React Router v6 and v7; v5 used <Switch> and useHistory.

Could you say it?
Full answer3 min read
JSX
import { BrowserRouter, Link, Route, Routes, useNavigate, useParams } from "react-router";

function UserPage() {
  const { id } = useParams();                 // "42" for /users/42: always a string
  const navigate = useNavigate();
  return (
    <>
      <h1>User {id}</h1>
      <button onClick={() => navigate("/users")}>Back to all users</button>
    </>
  );
}

function UserList() {
  return (
    <ul>
      <li><Link to="/users/42">Asha</Link></li>
      <li><Link to="/users/7">Ravi</Link></li>
    </ul>
  );
}

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/users" element={<UserList />} />
        <Route path="/users/:id" element={<UserPage />} />
        <Route path="*" element={<p>Page not found</p>} />
      </Routes>
    </BrowserRouter>
  );
}

Redirecting after an action

JSX
function LoginForm({ login }) {
  const navigate = useNavigate();
  async function handleSubmit(event) {
    event.preventDefault();
    await login(new FormData(event.currentTarget));
    navigate("/dashboard", { replace: true });   // replace: Back won't return to the login page
  }
  return <form onSubmit={handleSubmit}>{/* fields */}</form>;
}

Versions you'll see in tutorials

React Router v5v6 and v7
<Switch><Routes>
<Route component={UserPage} /><Route element={<UserPage />} />
useHistory().push("/x")useNavigate()("/x")
<Redirect to="/x" /><Navigate to="/x" />

In v7, everything is exported from the react-router package; react-router-dom still works and re-exports the same things.

Follow-ups

  • How do you read query strings like ?page=2? const [params, setParams] = useSearchParams(); params.get("page").
  • Why does refreshing /users/42 sometimes give a 404 in production? The server must send index.html for every route and let the client-side router take over; static hosts need a rewrite rule for that.
Q32Not marked yetFresherAsked oftenConcept

Client-side rendering vs server-side rendering vs static generation: how do they differ, and when would you use each in Next.js?

Say this first~30 sec to say

It's about where and when the HTML is built. CSR sends a near-empty page and the browser builds it: fine behind a login, weak for SEO. SSR builds HTML on the server per request: fresh and indexable, but costs server time on every visit. Static generation builds it once at build time and serves it from a CDN: the fastest, for content that rarely changes. Next.js lets each page choose.

Could you say it?
Full answer3 min read
CSRSSRSSGISR (incremental static regeneration)
HTML builtIn the browserOn the server, per requestAt build timeAt build time, then refreshed in the background
First loadSlowest: JavaScript firstFastFastest: from a CDNFastest
Data freshnessLiveLiveAs of the last buildAt most N seconds old
SEOWeakerStrongStrongStrong
Server costLowestHighestBuild time onlyLow
Good forDashboards behind a loginPersonalised or fast-changing pagesDocs, blogs, marketing pagesProduct and job listings

In the Next.js Pages Router

JSX
// pages/products/[id].jsx: static generation, refreshed at most every hour (ISR)
export async function getStaticProps({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`).then((r) => r.json());
  return { props: { product }, revalidate: 3600 };
}

export async function getStaticPaths() {
  return { paths: [], fallback: "blocking" };   // build each page on its first request
}

export default function ProductPage({ product }) {
  return <h1>{product.name}</h1>;
}

Use getServerSideProps instead of getStaticProps to render on every request.

In the Next.js App Router

Pages are React Server Components by default and fetch data directly in the component. Whether a route is static or dynamic depends on what it uses (cookies, headers, uncached data) and on the caching options you set:

JSX
// app/products/[id]/page.jsx
export const revalidate = 3600;                 // regenerate at most every hour

export default async function ProductPage({ params }) {
  const { id } = await params;
  const product = await fetch(`https://api.example.com/products/${id}`).then((r) => r.json());
  return <h1>{product.name}</h1>;
}

The App Router's caching defaults have changed across Next.js versions, and recent versions add an opt-in "use cache" model, so check the docs for the version you're using. The idea interviewers want is the same: each page chooses when its HTML is built.

Follow-ups

  • What is hydration? After the server sends HTML, React attaches event handlers and state to it in the browser, making the static markup interactive.
  • Can one app mix all of them? Yes, and most do: a static home page, ISR product pages, an SSR search page and a client-rendered account area.

09TypeScript basics

0/6 got it

Q33Not marked yetFresherAsked very oftenConcept

What does TypeScript add on top of JavaScript, and do its types still exist when the code runs?

Say this first~25 sec to say

TypeScript is JavaScript plus a static type system: you describe your data's shape, and the compiler catches mistakes before the code runs, like a misspelt property or a possibly undefined value, while editors get exact autocomplete. But types are erased: the output is plain JavaScript and nothing checks types at runtime, so data from APIs and forms still needs validating, for example with Zod.

Could you say it?
Full answer2 min read
TypeScript
interface User {
  id: number;
  name: string;
}

function greet(user: User): string {
  return `Hello, ${user.name.toUpperCase()}`;
}

greet({ id: 1, name: "Asha" });    // fine
greet({ id: 2, nmae: "Ravi" });    // error TS2353: Object literal may only specify known properties,
                                   // and 'nmae' does not exist in type 'User'.

The typo never reaches the browser: the build fails first.

Types disappear at runtime

TypeScript
const response = JSON.parse('{"id": "7", "name": null}') as User;   // "trust me": no check happens
console.log(typeof response.id);    // string: the type said number, and nothing checked

as User is an assertion, not a conversion. TypeScript believed it, and the compiled JavaScript has no trace of User at all.

Checking data at the boundary

TypeScript
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" && value !== null &&
    typeof (value as User).id === "number" &&
    typeof (value as User).name === "string"
  );
}

const data: unknown = JSON.parse('{"id": 7, "name": "Priya"}');
if (isUser(data)) {
  console.log(data.name.toUpperCase());   // PRIYA: TypeScript knows it's a User here
}

Libraries such as Zod let you write the schema once and get both the runtime check and the TypeScript type from it.

JavaScriptTypeScript
Type errors foundAt runtime, by usersAt compile time, by you
Editor helpGuessesExact autocomplete, go-to-definition, safe renames
Runs in the browserDirectlyAfter compiling to JavaScript
Runtime type checksNoneNone: the types are erased

Follow-ups

  • Does TypeScript make code slower? No: the types are removed at compile time. Only the build step costs time.
  • What does strict mode turn on? Checks such as strictNullChecks, so undefined and null must be handled explicitly, and noImplicitAny. New projects should always use it.
Q34Not marked yetFresherAsked very oftenConcept

type vs interface: what's the difference, and which would you use for component props?

Say this first~25 sec to say

Both describe object shapes, and either works for props. Interfaces extend with extends and merge when declared twice, which lets libraries add fields. Type aliases can't merge, but they can name any type: unions such as "primary" | "secondary", tuples, mapped and conditional types. A common rule: type for props and unions, interface for public object shapes others may extend.

Could you say it?
Full answer2 min read
TypeScript
// Both describe an object
interface ProductCardProps {
  name: string;
  price: number;
}

type ProductCardPropsAlias = {
  name: string;
  price: number;
};

// Extending
interface SaleCardProps extends ProductCardProps {
  discountPercent: number;
}
type SaleCardPropsAlias = ProductCardPropsAlias & { discountPercent: number };

// Only a type alias can name these
type Variant = "primary" | "secondary";     // a union
type Point = [x: number, y: number];        // a tuple
type Nullable<T> = T | null;                // a generic alias

// Only an interface merges
interface Window {
  analyticsId: string;                       // adds a field to the browser's global Window type
}

The last block is "declaration merging": it extends a type you don't own, here to tell TypeScript about a global your analytics script adds.

interfacetype
Object shapesYesYes
Unions, tuples, primitives, mapped typesNoYes
ExtendingextendsIntersections, &
Declaring twiceMergesError: duplicate identifier
Error messagesShow the interface's nameSometimes show the expanded type

Follow-ups

  • Can a class implement a type alias? Yes, if the alias describes an object shape, just like an interface.
  • Which do the React types use? Both. Your own props can be either; pick one convention for the codebase.
Q35Not marked yetFresherAsked oftenConcept

Union vs intersection types: how do they differ, and how do you narrow a union before using it?

Say this first~30 sec to say

A union, A | B, is one of several types; until you narrow it, you can only use what they share. An intersection, A & B, has all the properties of both. Narrow a union with typeof, in, instanceof, or best, a discriminated union: give every member a shared literal field, like kind: "card" or kind: "upi", and switch on it, so each branch knows exactly which fields exist.

Could you say it?
Full answer3 min read
TypeScript
// Narrowing with typeof
function formatAmount(amount: number | string): string {
  if (typeof amount === "number") {
    return `₹${amount.toFixed(2)}`;      // here amount is a number
  }
  return `₹${amount}`;                   // here it's a string
}

// A discriminated union
type CardPayment = { kind: "card"; cardLast4: string };
type UpiPayment = { kind: "upi"; upiId: string };
type CashPayment = { kind: "cash" };
type Payment = CardPayment | UpiPayment | CashPayment;

function describe(payment: Payment): string {
  switch (payment.kind) {
    case "card":
      return `Card ending ${payment.cardLast4}`;   // only card fields are available here
    case "upi":
      return `UPI ${payment.upiId}`;
    case "cash":
      return "Cash on delivery";
  }
}

// An intersection: has everything from both
type Timestamped = { createdAt: Date };
type SavedPayment = Payment & Timestamped;

console.log(formatAmount(1299));                               // ₹1299.00
console.log(describe({ kind: "upi", upiId: "asha@okbank" }));  // UPI asha@okbank

Inside case "card", writing payment.upiId is a compile error: TypeScript has narrowed the type to CardPayment.

Union A | BIntersection A & B
A value isEither an A or a BBoth an A and a B at once
Usable properties before narrowingOnly those common to bothAll of both
Typical use"One of these shapes or values"Combining types, mixins

Other narrowing tools

  • "upiId" in payment: checks whether the property exists.
  • error instanceof TypeError: for classes.
  • Truthiness: if (user) removes null and undefined.
  • Your own type guard, function isUser(x: unknown): x is User (question 33).

Follow-ups

  • What happens if you add a fourth payment kind? With an exhaustiveness check in the switch, the compiler flags every place that doesn't handle it (question 36).
  • What is string & number? never: no value can be both.
Q36Not marked yet1–3 yrsAsked oftenConcept

any vs unknown vs never: when would you use each one?

Say this first~25 sec to say

any switches type checking off, so mistakes slip through: a last resort while migrating JavaScript. unknown is the safe "could be anything": you must narrow it before use, which suits API responses, JSON.parse results and caught errors. never has no values: functions that always throw return it, and an exhaustive switch uses it so that a newly added union member becomes a compile error.

Could you say it?
Full answer3 min read
TypeScript
// any: no checks at all
let loose: any = 5;
loose.toUpperCase();                 // compiles, then crashes at runtime

// unknown: you must check first
let safe: unknown = JSON.parse('"hello"');
// safe.toUpperCase();               // error TS18046: 'safe' is of type 'unknown'.
if (typeof safe === "string") {
  console.log(safe.toUpperCase());   // HELLO: narrowed to string
}

// never: a function that never returns normally
function fail(message: string): never {
  throw new Error(message);
}

// never: exhaustiveness checking
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    default: {
      const unhandled: never = shape;   // only compiles if every kind is handled above
      return fail(`Unknown shape: ${JSON.stringify(unhandled)}`);
    }
  }
}

console.log(area({ kind: "square", side: 3 }));   // 9

If someone later adds { kind: "triangle"; base: number; height: number } to Shape, the const unhandled: never = shape line stops compiling with error TS2322: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'. The compiler points straight at the code that needs updating.

anyunknownnever
HoldsAnythingAnythingNothing
Can you use it directly?Yes, uncheckedNo: narrow it firstThere's nothing to use
Typical useEscape hatch while migratingExternal data, catch (error)Impossible branches, functions that always throw

Caught errors are unknown: with strict on, catch (error) gives error the type unknown, because anything can be thrown in JavaScript. Check error instanceof Error before reading error.message.

Follow-ups

  • How do you stop any spreading? Turn on noImplicitAny (part of strict), and use the @typescript-eslint/no-explicit-any lint rule.
  • What's the difference between unknown and {}? {} accepts any value except null and undefined; unknown accepts everything.
Q37Not marked yet1–3 yrsAsked oftenCoding

Write a generic function that returns the first item of an array, typed so the result matches the element type, and explain why that's better than using any

Say this first~25 sec to say

A generic function takes a type parameter: function first<T>(items: T[]): T | undefined. TypeScript infers T from the argument, so first([1, 2, 3]) returns number | undefined and first(users) returns User | undefined, and the caller keeps full type checking. With any, the return type is any, so a typo such as first(users).nmae compiles and fails at runtime. Return T | undefined because the array might be empty.

Could you say it?
Full answer2 min read
TypeScript
function first<T>(items: T[]): T | undefined {
  return items[0];
}

interface User {
  id: number;
  name: string;
}

const users: User[] = [{ id: 1, name: "Asha" }];

const n = first([10, 20, 30]);      // number | undefined
const u = first(users);             // User | undefined
console.log(n, u?.name);            // 10 Asha
console.log(first([]));             // undefined: why the return type includes undefined

// first(users)?.nmae;              // error TS2339: Property 'nmae' does not exist on type 'User'.

function firstAny(items: any[]): any {
  return items[0];
}
firstAny(users).nmae;               // compiles happily, and is undefined at runtime

Constraining a type parameter

TypeScript
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;     // T must have a length
}

console.log(longest("chai", "coffee"));     // coffee
console.log(longest([1, 2], [1, 2, 3]));    // [ 1, 2, 3 ]
// longest(10, 20);                         // error: number has no 'length'

Generics you already use: Array<T>, Promise<T>, useState<T>(), Map<K, V>.

Follow-ups

  • Why not items: unknown[]? The function would be safe, but the caller would get back unknown and lose the element type; generics keep it.
  • What about tuples? For "the first element of this exact tuple", function head<T extends unknown[]>(items: T): T[0] preserves the precise type.
Q38Not marked yet1–3 yrsAsked sometimesConcept

What do Partial, Pick and Omit do, and where would you use each, starting from a single User type?

Say this first~30 sec to say

Utility types build new types from an existing one, so each field is declared once. Partial<User> makes every field optional, which suits update payloads such as a PATCH body. Pick<User, "id" | "name"> keeps only the listed fields, say for a list row or public profile. Omit<User, "password"> keeps everything except the listed fields, say for data you send to the browser. Change User, and every derived type follows.

Could you say it?
Full answer2 min read
TypeScript
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  city: string;
}

type UserUpdate = Partial<Omit<User, "id">>;      // any fields except id, all optional
type UserListItem = Pick<User, "id" | "name">;    // just what a list row needs
type PublicUser = Omit<User, "password">;         // safe to send to the browser

function updateUser(id: number, changes: UserUpdate) {
  return { id, ...changes };
}

const row: UserListItem = { id: 1, name: "Asha" };
const profile: PublicUser = { id: 1, name: "Asha", email: "[email protected]", city: "Pune" };

console.log(updateUser(1, { city: "Bengaluru" }));   // { id: 1, city: 'Bengaluru' }
console.log(row.name, profile.city);                  // Asha Pune
// const leak: PublicUser = { ...profile, password: "secret" };   // error: 'password' isn't in PublicUser

Other utility types worth knowing

UtilityResult
Required<T>Every field required
Readonly<T>Every field read-only
Record<K, V>An object with keys K and values V, e.g. Record<"pune" | "delhi", number>
ReturnType<typeof fn>The return type of a function
Parameters<typeof fn>A function's parameter types, as a tuple
NonNullable<T>T without null and undefined
Awaited<T>The value a promise resolves to

Follow-ups

  • How is Partial written? As a mapped type: type Partial<T> = { [K in keyof T]?: T[K] }.
  • Pick or Omit, which is safer? Pick lists what's included, so new fields added to User stay out until you add them. Omit lets new fields in automatically, which could leak a sensitive one.

10TypeScript with React

0/2 got it

Q39Not marked yetFresherAsked oftenCoding

Type the props of a Button component that takes an optional variant ('primary' or 'secondary'), an optional disabled flag, an onClick handler and children

Say this first~25 sec to say

Declare a props type with an optional variant typed as the union "primary" | "secondary", an optional disabled boolean, onClick typed as React.MouseEventHandler<HTMLButtonElement>, and children: React.ReactNode, then give the optional props defaults in the destructuring. Type children explicitly: since the React 18 types, React.FC no longer adds it for you. To accept every native button attribute too, build the props from React.ComponentProps<"button">.

Could you say it?
Full answer3 min read
TSX
import type { MouseEventHandler, ReactNode } from "react";

type ButtonProps = {
  variant?: "primary" | "secondary";
  disabled?: boolean;
  onClick: MouseEventHandler<HTMLButtonElement>;
  children: ReactNode;
};

export function Button({ variant = "primary", disabled = false, onClick, children }: ButtonProps) {
  return (
    <button type="button" className={`btn btn-${variant}`} disabled={disabled} onClick={onClick}>
      {children}
    </button>
  );
}

export function Example() {
  return (
    <>
      <Button onClick={() => console.log("saved")}>Save</Button>
      <Button variant="secondary" disabled onClick={(e) => console.log(e.currentTarget.textContent)}>
        Cancel
      </Button>
      {/* <Button variant="danger" onClick={() => {}}>Delete</Button>   error: "danger" isn't allowed */}
    </>
  );
}

e in the second onClick is inferred as React.MouseEvent<HTMLButtonElement>, so e.currentTarget is a button.

Accepting every native button prop

TSX
import type { ComponentProps } from "react";

type FlexibleButtonProps = ComponentProps<"button"> & {
  variant?: "primary" | "secondary";
};

export function FlexibleButton({ variant = "primary", className = "", ...rest }: FlexibleButtonProps) {
  return <button className={`btn btn-${variant} ${className}`} {...rest} />;
}

// <FlexibleButton type="submit" aria-label="Save" form="profile">Save</FlexibleButton>

Why not React.FC<ButtonProps>? It works, but it adds nothing important. Since the React 18 types it no longer includes children automatically, and most codebases now type the props parameter directly, as above.

Follow-ups

  • ReactNode or ReactElement for children? ReactNode accepts anything renderable: text, numbers, elements, arrays, null. ReactElement accepts only a single element.
  • How do you require either href or onClick, but not both? A union of two prop types: { href: string; onClick?: never } | { onClick: () => void; href?: never }.
Q40Not marked yetFresherAsked oftenCoding

Type a search box component: a results state that starts as null, the input's onChange handler, and a ref used to focus the input

Say this first~30 sec to say

Give useState a type argument when the initial value doesn't describe all the later ones: useState<Product[] | null>(null), where null means "not searched yet". Type the change handler's event as React.ChangeEvent<HTMLInputElement>, so event.target.value is a string. Create the ref with useRef<HTMLInputElement>(null) and attach it to the input; its current is HTMLInputElement | null, so check it before focusing. React 19's types require an argument to useRef, hence the explicit null.

Could you say it?
Full answer3 min read
TSX
import { useRef, useState } from "react";
import type { ChangeEvent, FormEvent } from "react";

type Product = { id: number; name: string };

type SearchBoxProps = {
  search: (query: string) => Promise<Product[]>;
};

export function SearchBox({ search }: SearchBoxProps) {
  const [query, setQuery] = useState("");                        // inferred as string
  const [results, setResults] = useState<Product[] | null>(null); // null: not searched yet
  const inputRef = useRef<HTMLInputElement>(null);               // React 19 types need the null

  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    setQuery(event.target.value);                                // a string
  }

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setResults(await search(query));
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} aria-label="Search" value={query} onChange={handleChange} />
      <button type="submit">Search</button>
      <button type="button" onClick={() => inputRef.current?.focus()}>Focus</button>

      {results === null ? (
        <p>Type and press Search.</p>
      ) : results.length === 0 ? (
        <p>No results.</p>
      ) : (
        <ul>{results.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
      )}
    </form>
  );
}

What each annotation buys

CodeWithout itWith it
useState<Product[] | null>(null)Inferred as null forever: setResults([...]) is an errorCan be null or an array
ChangeEvent<HTMLInputElement>event is implicitly any under strictevent.target.value is a string
useRef<HTMLInputElement>(null)current is null onlycurrent is HTMLInputElement | null
inputRef.current?.focus()Safe even before the input has mounted

The three states of results (null, empty, filled) let the UI tell "haven't searched yet" apart from "searched, found nothing", which a plain empty array can't.

Follow-ups

  • What type does useRef return when you pass null? RefObject<HTMLInputElement | null> in React 19's types: React fills current in when the input mounts.
  • How do you type a handler for a <select>? ChangeEvent<HTMLSelectElement>; for a textarea, ChangeEvent<HTMLTextAreaElement>.

Last step

Write these from memory

Before a React interview, make sure you can write each of these on a blank page without help:

  • Q7
  • Q9
  • Q12
  • Q15
  • Q17
  • Q19
  • Q21
  • Q23
  • Q28
  • Q36
  • Q37
  • Q39
  • Q40

0 of 13 done