React Frontend Interview Questions with Code Examples

2025-08-20

React interviews are easier when you recognize the patterns hiding under each question. Below are practical questions you will likely face, along with short, runnable examples and the talking points an interviewer wants to hear.


1) How do you manage state efficiently in React

What to say: prefer local state for UI specifics, context for cross cutting data, and a server state tool when data comes from APIs. Keep state minimal and derive the rest.

Example – local, derived, and memoized values

import { useMemo, useState } from "react";

export default function Cart() {
  const [items, setItems] = useState([{ id: 1, price: 10, qty: 2 }]);
  const total = useMemo(
    () => items.reduce((sum, it) => sum + it.price * it.qty, 0),
    [items]
  );

  function addOne() {
    setItems(prev => prev.map(it => it.id === 1 ? { ...it, qty: it.qty + 1 } : it));
  }

  return (
    <div>
      <button onClick={addOne}>Add one</button>
      <p>Total: ${total}</p>
    </div>
  );
}

Key points: keep only items in state, compute total from it. useMemo avoids recalculations on unrelated renders.


2) When should you use useEffect and when should you not

What to say: effects are for syncing with systems outside React like network calls, subscriptions, timers, or imperative APIs. Do not use effects to compute values you can derive during render.

Example – fetch on key change with cleanup

import { useEffect, useState } from "react";

export default function User({ id }) {
  const [user, setUser] = useState(null);
  const [err, setErr] = useState("");

  useEffect(() => {
    let ignore = false;
    setErr("");
    setUser(null);

    fetch(`/api/users/${id}`)
      .then(r => r.ok ? r.json() : Promise.reject(r.statusText))
      .then(data => { if (!ignore) setUser(data); })
      .catch(e => { if (!ignore) setErr(String(e)); });

    return () => { ignore = true; };
  }, [id]);

  if (err) return <p>Error: {err}</p>;
  if (!user) return <p>Loading...</p>;
  return <p>{user.name}</p>;
}

Pitfall checklist: correct dependency array, cleanup function, avoid setting state if the component is already unmounted.


3) Explain reconciliation and keys

What to say: keys let React identify which children changed so it can re order instead of re mount. Stable, unique keys per list item avoid lost state and weird focus bugs.

Example – correct key use

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(t => (
        <li key={t.id}>
          <input defaultValue={t.text} />
        </li>
      ))}
    </ul>
  );
}

Avoid using array index when items can be inserted or removed.


4) Optimize re renders with React.memo, useCallback, and useMemo

What to say: memoization helps when child components are expensive and props are stable. Do not sprinkle callbacks everywhere without measuring.

import { memo, useCallback, useState } from "react";

const Row = memo(function Row({ item, onSelect }) {
  return <div onClick={() => onSelect(item.id)}>{item.label}</div>;
});

export default function List({ data }) {
  const [sel, setSel] = useState(null);
  const onSelect = useCallback(id => setSel(id), []);
  return (
    <><p>Selected: {sel}</p>
      {data.map(d => <Row key={d.id} item={d} onSelect={onSelect} />)}
    </>
  );
}

Rule of thumb: measure first, then memoize the hot path.


5) Controlled vs uncontrolled components

What to say: controlled inputs use React state as the single source of truth, which enables validation and conditional UI. Uncontrolled inputs rely on the DOM and ref.

// Controlled
function Controlled() {
  const [email, setEmail] = useState("");
  const valid = email.includes("@");
  return (
    <label>
      Email
      <input value={email} onChange={e => setEmail(e.target.value)} />
      {!valid && <span role="alert">Invalid email</span>}
    </label>
  );
}

Use uncontrolled with defaultValue for large forms when you only read values on submit.


6) Error boundaries and handling failures

What to say: error boundaries catch render time errors in child trees. They do not catch async errors in event handlers. In React 18, use a class boundary or a tiny wrapper library.

import React from "react";

class Boundary extends React.Component {
  constructor(props) { super(props); this.state = { hasError: false }; }
  static getDerivedStateFromError() { return { hasError: true }; }
  componentDidCatch(err, info) { console.error(err, info); }
  render() {
    if (this.state.hasError) return <p>Something went wrong</p>;
    return this.props.children;
  }
}

Wrap the page or a risky widget with Boundary.


7) Suspense and data fetching at the component level

What to say: Suspense lets you declaratively show a fallback while data loads. Pair it with a cache or library that throws a pending promise during read.

// Pseudo resource
const userCache = new Map();
function readUser(id) {
  if (userCache.has(id)) return userCache.get(id);
  throw fetch(`/api/users/${id}`)
    .then(r => r.json())
    .then(u => userCache.set(id, u));
}

function UserCard({ id }) {
  const user = readUser(id); // throws while pending
  return <div>{user.name}</div>;
}

export default function Page() {
  return (
    <React.Suspense fallback={<p>Loading user...</p>}>
      <UserCard id="42" />
    </React.Suspense>
  );
}

Interview angle: explain how Suspense coordinates UI without hand written loading flags.


8) Accessibility essentials

What to say: semantic HTML first, correct ARIA roles only when needed, keyboard support, visible focus, and color contrast.

function Modal({ open, onClose, title, children }) {
  if (!open) return null;
  return (
    <div role="dialog" aria-modal="true" aria-labelledby="m-title">
      <h2 id="m-title">{title}</h2>
      <button onClick={onClose} aria-label="Close modal">×</button>
      {children}
    </div>
  );
}

Test with keyboard only and a screen reader simulator.


9) Performance patterns for long lists

What to say: virtualization, pagination, and incremental rendering cut work significantly.

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

// Tiny windowed list without a library for demonstration
function WindowedList({ items, itemHeight = 32, height = 200 }) {
  const [scrollTop, setScrollTop] = useState(0);
  const visibleCount = Math.ceil(height / itemHeight) + 2;
  const start = Math.floor(scrollTop / itemHeight);
  const end = Math.min(items.length, start + visibleCount);
  const offsetY = start * itemHeight;

  return (
    <divstyle={{ overflowY: "auto", height }}
      onScroll={e => setScrollTop(e.currentTarget.scrollTop)}
    >
      <div style={{ height: items.length * itemHeight, position: "relative" }}>
        <div style={{ position: "absolute", top: offsetY, left: 0, right: 0 }}>
          {items.slice(start, end).map((it, i) => (
            <div key={it.id} style={{ height: itemHeight }}>{it.label}</div>
          ))}
        </div>
      </div>
    </div>
  );
}

Call out libraries like react-window or react-virtualized in real work.


10) Testing React components

What to say: test behavior not implementation. Use React Testing Library to render, act, and assert on the screen.

// Example test with RTL and Jest
import { render, screen, fireEvent } from "@testing-library/react";
import Cart from "./Cart";

test("adds one item", () => {
  render(<Cart />);
  fireEvent.click(screen.getByText(/add one/i));
  expect(screen.getByText(/total: \$/i)).toHaveTextContent("$30");
});

Mock network boundaries, not internal component details.


11) Forms with validation

What to say: build small controlled forms by hand, use a form library when forms are large or dynamic.

import { useState } from "react";

function Signup() {
  const [form, setForm] = useState({ email: "", pwd: "" });
  const [errors, setErrors] = useState({});

  function onChange(e) {
    const { name, value } = e.target;
    setForm(f => ({ ...f, [name]: value }));
  }

  function onSubmit(e) {
    e.preventDefault();
    const errs = {};
    if (!form.email.includes("@")) errs.email = "Invalid email";
    if (form.pwd.length < 8) errs.pwd = "Min 8 chars";
    setErrors(errs);
    if (Object.keys(errs).length === 0) {
      // submit
    }
  }

  return (
    <form onSubmit={onSubmit}>
      <label>Email
        <input name="email" value={form.email} onChange={onChange} />
      </label>
      {errors.email && <p role="alert">{errors.email}</p>}
      <label>Password
        <input name="pwd" type="password" value={form.pwd} onChange={onChange} />
      </label>
      {errors.pwd && <p role="alert">{errors.pwd}</p>}
      <button>Create account</button>
    </form>
  );
}

Mention react-hook-form and schema validators like Zod or Yup.


12) SSR, hydration, and Next.js basics

What to say: SSR renders HTML on the server for faster first paint and SEO. Hydration attaches event handlers on the client. Data that differs between server and client must be handled carefully to avoid hydration mismatches.

Talking points:

  • Put time based or random values inside useEffect or guard with useClient in frameworks that support it
  • Stream with Suspense boundaries for progressive hydration
  • Cache server data and revalidate when needed

13) Security and safe rendering

What to say: avoid dangerouslySetInnerHTML unless sanitized. Escape user input by default. Protect tokens and secrets using HTTP only cookies. Validate props for external images or links.

function SafeHtml({ html }) {
  // Assume html is sanitized on the server
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

Call out CSP, trusted types, and input validation where relevant.


14) Concurrency and race conditions in UI

What to say: stale responses can clobber newer state. Track the latest request and ignore older ones.

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

function SearchBox() {
  const [q, setQ] = useState("");
  const [results, setResults] = useState([]);
  const ticket = useRef(0);

  useEffect(() => {
    if (!q) { setResults([]); return; }
    const id = ++ticket.current;
    const ctrl = new AbortController();

    fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: ctrl.signal })
      .then(r => r.json())
      .then(data => { if (id === ticket.current) setResults(data); })
      .catch(() => {});

    return () => ctrl.abort();
  }, [q]);

  return (
    <><input value={q} onChange={e => setQ(e.target.value)} placeholder="Search..." />
      <ul>{results.map(r => <li key={r.id}>{r.label}</li>)}</ul>
    </>
  );
}

15) TypeScript in React interviews

What to say: use generics for hooks and components, prefer discriminated unions for component states, and type your events and refs.

import { useRef } from "react";

type State =
  | { kind: "idle" }
  | { kind: "loading" }
  | { kind: "error"; msg: string }
  | { kind: "success"; data: string[] };

function Table<T extends { id: string }>({ rows }: { rows: T[] }) {
  return <ul>{rows.map(r => <li key={r.id}>{JSON.stringify(r)}</li>)}</ul>;
}

function FocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  return <input ref={inputRef} onFocus={() => console.log("focus")} />;
}

Rapid fire questions you can practice

  • What re render triggers exist in React
  • How do you prevent prop drilling without global state everywhere
  • How do you cancel a fetch when a component unmounts
  • Explain Suspense vs a manual loading flag
  • When would you choose context over a store library
  • What is the difference between useLayoutEffect and useEffect
  • How do you measure and fix slow renders in DevTools
  • What are hydration mismatches and how do you avoid them

Final note

Interviews reward clarity. State your plan in one sentence, write a small slice of code that works, then iterate. If you want a way to compare your approach to a clean reference while you practice React problems, you can use a helper that overlays a concise explanation and code so you can learn the pattern quickly and then explain it back. When you need that kind of boost, try StealthCoder for guided practice: https://stealthcoder.app