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 withuseClient
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
anduseEffect
- 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