React Interview Questions 2026

Top 50 Questions & Answers — Hooks, State Management, Performance, Next.js, TypeScript & Testing

This guide covers the most frequently asked React interview questions in 2026 — from core concepts and hooks to advanced patterns, performance optimisation, Next.js, TypeScript, and testing strategies.

Easy = Core concepts, basic hooks  |  Medium = Patterns, optimisation, state management  |  Hard = Architecture, advanced internals, production
Fundamentals & JSX
1
What is React and what problem does it solve?Easy

React is a JavaScript library for building user interfaces through a component-based model. It solves the problem of keeping the DOM in sync with application state.

  • Declarative — you describe what the UI should look like for a given state; React figures out the minimal DOM changes needed.
  • Component-based — encapsulated components manage their own state and compose to build complex UIs. Encourages reuse.
  • Virtual DOM — React maintains a lightweight in-memory representation of the DOM. On state change it diffs the new virtual DOM against the previous one and applies only the minimal set of real DOM changes (reconciliation).
  • Unidirectional data flow — data flows down via props; events flow up via callbacks. Predictable and debuggable.
2
What is JSX and how does it compile?Easy

JSX (JavaScript XML) is a syntax extension that lets you write HTML-like markup inside JavaScript. Babel compiles it to React.createElement() calls.

// JSX:
const element = <h1 className="title">Hello {name}</h1>;

// Compiled (React 17+ automatic JSX transform — no React import needed):
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx("h1", { className: "title", children: `Hello ${name}` });

// Key JSX rules:
// - Must return a single root element (or Fragment)
// - class → className, for → htmlFor (reserved JS keywords)
// - Self-close all tags: <img />, <br />, <Component />
// - Expressions in {}, not statements
// - Comments: {/* comment */}
3
What is the difference between a controlled and an uncontrolled component?Easy
  • Controlled — the React component controls the form element's value through state. Every keystroke updates state; state drives the input value. Single source of truth. Enables validation, conditional disabling, formatted input.
  • Uncontrolled — the DOM maintains the form element's own state. You access the value via a ref when needed (e.g. on submit). Simpler for basic forms, but harder to validate or manipulate programmatically.
// Controlled:
const [name, setName] = useState('');
<input value={name} onChange={e => setName(e.target.value)} />

// Uncontrolled:
const nameRef = useRef(null);
<input ref={nameRef} defaultValue="John" />
// Access: nameRef.current.value on submit
React Hook Form uses uncontrolled inputs under the hood for performance — avoids re-rendering on every keystroke.
4
What is the Virtual DOM and how does reconciliation work?Medium

The Virtual DOM is a JavaScript object tree that mirrors the real DOM. On each render, React builds a new virtual DOM tree and diffs it against the previous one using its reconciliation algorithm (Fiber).

Diffing heuristics:

  • Elements of different types → tear down old tree, build new one
  • Same element type → update only changed attributes
  • Lists → use key prop to match old and new children. Without keys, React diffs by position (may produce wrong mutations).
// Without key: React sees "3 <li>s" each time, re-renders all
// With key: React sees which item was added/removed/moved
{items.map(item => (
  <li key={item.id}>{item.name}</li>  // key should be stable unique ID
))}

React Fiber (React 16+) makes reconciliation interruptible — it can pause work, prioritise urgent updates (user input), and resume background work. This enables Concurrent Mode features like useTransition and Suspense.

5
What is the purpose of the key prop?Easy

Keys help React identify which items in a list have changed, been added, or removed. React uses keys to match virtual DOM elements between renders.

// WRONG: using index as key — breaks reordering and deletion
{items.map((item, i) => <Item key={i} {...item} />)}

// RIGHT: use a stable, unique ID
{items.map(item => <Item key={item.id} {...item} />)}

// Keys must be unique among siblings (not globally unique)
// Keys are not passed as props — use a separate prop if the child needs the ID

When index is OK: static lists that never reorder or filter (e.g. a fixed list of navigation tabs). For any dynamic list, use a real ID.

Trick: you can use key to force a component to fully remount when a value changes — useful for resetting all internal state:

<UserProfile key={userId} userId={userId} />
// Changing userId forces complete remount, resetting all hooks
6
What are props and how do you validate them?Easy

Props (properties) are the mechanism for passing data from parent to child components. They are read-only — a component must never modify its own props.

// TypeScript (preferred in 2026 — compile-time type safety):
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary';  // optional with union type
  disabled?: boolean;
}
const Button = ({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) => (
  <button className={`btn-${variant}`} onClick={onClick} disabled={disabled}>
    {label}
  </button>
);

// PropTypes (JS only — runtime validation, less common now):
Button.propTypes = {
  label: PropTypes.string.isRequired,
  onClick: PropTypes.func.isRequired,
  variant: PropTypes.oneOf(['primary', 'secondary']),
};
7
What is prop drilling and how do you avoid it?Medium

Prop drilling is passing props through many intermediate components that don't use them — only pass them down to a deeply nested child.

// Problem: intermediary B and C don't use theme, just pass it
<A theme="dark">
  <B theme="dark">      // B doesn't care
    <C theme="dark">    // C doesn't care
      <D theme="dark" /> // D actually needs it
    </C>
  </B>
</A>

// Solution 1: Context (for genuinely global/shared state)
const ThemeContext = React.createContext('light');
// Wrap: <ThemeContext.Provider value="dark">...</ThemeContext.Provider>
// Consume: const theme = useContext(ThemeContext);

// Solution 2: Component composition (often overlooked, very powerful)
// Pass the component that needs the data instead of the data
<Layout sidebar={<Sidebar theme={theme} />} />
// Layout doesn't know about theme at all

// Solution 3: State management library (Zustand, Redux) for complex cases
8
What are React Fragments and when do you use them?Easy

Fragments let you return multiple elements without adding an extra DOM node.

// Without Fragment — adds unnecessary div to DOM:
return (
  <div>
    <td>Cell 1</td>
    <td>Cell 2</td>
  </div>  // invalid: div inside tr!
);

// With Fragment:
return (
  <React.Fragment>
    <td>Cell 1</td>
    <td>Cell 2</td>
  </React.Fragment>
);

// Shorthand (most common):
return (
  <>
    <td>Cell 1</td>
    <td>Cell 2</td>
  </>
);

// Note: shorthand <></> cannot have key prop.
// Use <React.Fragment key={id}> when rendering lists of fragments.
9
What is the component lifecycle in React (functional components)?Medium

Functional components with hooks map to the same lifecycle phases as class components:

function MyComponent({ id }) {
  const [data, setData] = useState(null);

  // Mount + update: runs after every render by default
  useEffect(() => {
    console.log('Rendered');
  });

  // Mount only (empty deps array):
  useEffect(() => {
    console.log('Mounted — equivalent to componentDidMount');
    return () => {
      console.log('Unmounted — equivalent to componentWillUnmount');
    };
  }, []);

  // Update when id changes (equivalent to componentDidUpdate for id):
  useEffect(() => {
    fetchData(id).then(setData);
    return () => cancelFetch();  // cleanup before re-running or unmount
  }, [id]);

  // Render phase: the function body itself
  return <div>{data}</div>;
}

Execution order: render → paint DOM → run effects. Effects run after paint so they don't block the browser. Use useLayoutEffect if you need to run synchronously after DOM mutation but before paint (rare — DOM measurements).

10
What is the difference between class components and functional components?Easy
  • Functional components are plain JavaScript functions. With hooks (React 16.8+) they have full capabilities — state, effects, context, refs, memoization.
  • Class components extend React.Component, use this.state and lifecycle methods. Still supported but no new features added.
// Class (legacy):
class Counter extends React.Component {
  state = { count: 0 };
  increment = () => this.setState({ count: this.state.count + 1 });
  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}

// Functional (modern — preferred):
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Write all new components as functions. Class components are fine to maintain but React's new features (Server Components, Actions, Concurrent features) are hooks-first.
Hooks In Depth
11
What are the rules of hooks?Easy
  1. Only call hooks at the top level — not inside loops, conditions, or nested functions. React relies on the order of hook calls to associate state with the right hook between renders.
  2. Only call hooks from React functions — functional components or custom hooks (functions starting with use). Not regular JS functions, class methods, or event handlers.
// WRONG: conditional hook call
if (condition) {
  const [value, setValue] = useState(0);  // breaks hook ordering!
}

// RIGHT: hook at top, condition inside
const [value, setValue] = useState(0);
useEffect(() => {
  if (condition) { /* logic */ }
}, [condition]);

The eslint-plugin-react-hooks enforces these rules automatically. Always install it in your project.

12
Explain useState — batching, functional updates, lazy initialisation.Medium
const [count, setCount] = useState(0);

// Functional update: always use when next state depends on previous
setCount(prev => prev + 1);  // safe
setCount(count + 1);          // stale closure risk in async code

// Lazy initialisation (expensive computation only on mount):
const [data, setData] = useState(() => parseHeavyData(rawInput));
// The function is called once, not on every render

// Batching (React 18+): multiple setState calls in one event are batched
// into a single re-render automatically — even inside setTimeout, Promises
function handleClick() {
  setCount(c => c + 1);    // React 18: batched together
  setName('Alice');         // single re-render for both
}

// Force immediate update (rare — avoid): use flushSync
import { flushSync } from 'react-dom';
flushSync(() => setCount(c => c + 1));
// DOM is updated before this line
13
Explain useEffect — cleanup, dependency array pitfalls, strict mode double invoke.Medium
useEffect(() => {
  const controller = new AbortController();

  async function fetchUser() {
    try {
      const res = await fetch(`/api/users/${userId}`, { signal: controller.signal });
      const data = await res.json();
      setUser(data);
    } catch (e) {
      if (e.name !== 'AbortError') setError(e);
    }
  }
  fetchUser();

  return () => controller.abort();  // cleanup: cancel fetch on unmount or re-run
}, [userId]);  // runs when userId changes

Dependency array pitfalls:

  • Omit a dependency → stale closure (reads old value forever)
  • Include an object/function created during render → infinite loop (new reference every render)
  • Fix: memoize objects with useMemo, functions with useCallback; or move them inside the effect

Strict Mode double invoke (React 18 dev): React mounts → unmounts → remounts every component to help catch missing cleanup. Your effect runs twice in dev. This is intentional — fix your cleanup, don't disable Strict Mode.

14
What is useRef and when do you use it instead of state?Medium

useRef returns a mutable object ({ current: value }) that persists across renders but changing it does NOT trigger a re-render.

// Use case 1: DOM element reference
const inputRef = useRef(null);
<input ref={inputRef} />
inputRef.current.focus();  // programmatic focus

// Use case 2: store value without causing re-render
const timerRef = useRef(null);
timerRef.current = setInterval(() => {}, 1000);
// On cleanup:
clearInterval(timerRef.current);

// Use case 3: access latest state in a closure (avoid stale closure)
const latestCount = useRef(count);
useEffect(() => { latestCount.current = count; });  // always current
const handleTimeout = useCallback(() => {
  console.log(latestCount.current);  // always latest value
}, []);  // no stale closure

ref vs state: use state when the value should trigger UI updates; use ref when it's internal bookkeeping (timer IDs, previous values, DOM nodes) that doesn't affect rendering.

15
What is useContext and when should you avoid it?Medium
// Create context with a sensible default:
const ThemeContext = createContext<'light' | 'dark'>('light');

// Provider (wraps the subtree that needs the value):
function App() {
  const [theme, setTheme] = useState<'light' | 'dark'>('dark');
  return (
    <ThemeContext.Provider value={theme}>
      <Layout />
    </ThemeContext.Provider>
  );
}

// Consumer (any descendant):
function Button() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Click</button>;
}

When to avoid:

  • High-frequency updates — every context consumer re-renders when the value changes. A counter updated 60 times/second in context re-renders every subscriber. Use Zustand/Jotai instead.
  • Large objects — putting your entire app state in one context means any state change re-renders everything. Split contexts by update frequency.
  • Complex derived state — context doesn't have selectors. Zustand/Redux Toolkit have selector optimisation.
16
What is useReducer and when do you choose it over useState?Medium
type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset'; payload: number };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'reset':     return action.payload;
    default: throw new Error(`Unknown action: ${action.type}`);
  }
}

const [count, dispatch] = useReducer(reducer, 0);
dispatch({ type: 'increment' });
dispatch({ type: 'reset', payload: 10 });

Choose useReducer when:

  • Complex state with multiple sub-values that change together
  • Next state depends on the previous in non-trivial ways
  • Many different update operations that benefit from named actions
  • You want to test state transitions in isolation (reducer is a pure function)
  • You're building a mini-Redux for local complex state
17
What is useMemo and useCallback — when do they help vs hurt?Medium
// useMemo: memoize an expensive computed value
const sortedList = useMemo(
  () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
  [items]  // recompute only when items changes
);

// useCallback: memoize a function reference
const handleDelete = useCallback((id: string) => {
  setItems(prev => prev.filter(item => item.id !== id));
}, []);  // stable reference — doesn't change between renders

When they help:

  • useMemo: genuinely expensive computation (sorting 10k items, parsing CSV, heavy math)
  • useCallback: function passed to a memoized child (React.memo) or as a useEffect dependency

When they hurt (premature optimisation):

  • Cheap computations — memo overhead (comparison + cache) can exceed the cost of recomputing
  • Values that change every render anyway (the memo never hits)
  • They add cognitive overhead — don't memoize by default; profile first
18
How do you create a custom hook and what are the naming conventions?Medium

Custom hooks are plain JavaScript functions that start with use and can call other hooks. They extract reusable stateful logic from components.

// Custom hook: useDebounce
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Usage:
function SearchBox() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) search(debouncedQuery);
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

Naming convention: always start with use — React lint rules use this prefix to identify hooks. Popular custom hooks to know: useLocalStorage, useFetch, useIntersectionObserver, usePrevious, useEventListener.

19
What is useTransition and useDeferredValue?Medium

React 18 Concurrent Mode hooks for keeping the UI responsive during expensive updates.

// useTransition: mark a state update as non-urgent
const [isPending, startTransition] = useTransition();

function handleSearch(e) {
  const value = e.target.value;
  setInputValue(value);  // urgent: update input immediately
  startTransition(() => {
    setSearchResults(filterItems(value));  // non-urgent: can be interrupted
  });
}
// isPending = true while the transition is processing → show spinner

// useDeferredValue: defer a value until urgent updates settle
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  // deferredQuery lags behind query during fast typing
  // avoids expensive re-render on every keystroke
  const results = useMemo(
    () => filterItems(deferredQuery),
    [deferredQuery]
  );
  return <List items={results} />;
}

Difference: useTransition wraps the state setter (you control what's deferred). useDeferredValue wraps the value (useful when you don't control the setter — e.g. prop from parent).

20
What is useId, useImperativeHandle, and useInsertionEffect?Medium
// useId: generate stable unique IDs for accessibility (SSR-safe)
function FormField({ label }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  );
}
// Generates ":r0:", ":r1:" etc. — stable between server and client renders

// useImperativeHandle: expose imperative methods from a component to parent
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    shake: () => inputRef.current.animate([...], { duration: 300 }),
  }));
  return <input ref={inputRef} />;
});
// Parent: inputRef.current.shake()

// useInsertionEffect: CSS-in-JS libraries only
// Runs before any DOM mutations — lets libs inject styles before layout
21
What is Suspense and how does it work with data fetching?Hard
// Suspense: show fallback while child is "suspended" (not ready)
<Suspense fallback={<Spinner />}>
  <UserProfile userId={userId} />
</Suspense>

// A component suspends by throwing a Promise.
// React catches the thrown Promise, shows fallback,
// then re-renders when the Promise resolves.

// With React Query (v5+):
function UserProfile({ userId }) {
  const { data } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });
  // data is always defined here — no loading/error state needed
  return <div>{data.name}</div>;
}

// With Next.js App Router:
// async Server Components suspend automatically
async function UserPage({ params }) {
  const user = await fetchUser(params.id);  // suspends naturally
  return <UserProfile user={user} />;
}

Pair with ErrorBoundary to catch rejected Promises. React 18 also enables streaming SSR with Suspense — the server streams HTML as each Suspense boundary resolves.

22
What is an Error Boundary and how do you implement one?Medium

Error Boundaries catch JavaScript errors in their child component tree during rendering, lifecycle methods, and constructors. They must be class components (no hook equivalent for componentDidCatch yet).

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, info) {
    logErrorToService(error, info.componentStack);
  }

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

// Usage:
<ErrorBoundary fallback={<ErrorPage />}>
  <Dashboard />
</ErrorBoundary>

// Use react-error-boundary library for a hooks-friendly API:
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary FallbackComponent={ErrorPage} onError={logError}>
Error Boundaries do NOT catch errors in event handlers (use try/catch), async code, or SSR. They only catch render-time errors.
State Management
23
What is Zustand and why is it popular over Redux?Medium
import { create } from 'zustand';

interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  total: () => number;
}

const useCart = create<CartStore>((set, get) => ({
  items: [],
  addItem: (item) => set(state => ({ items: [...state.items, item] })),
  removeItem: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) })),
  total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));

// In component — only re-renders when items changes:
const items = useCart(state => state.items);
const addItem = useCart(state => state.addItem);

Why Zustand over Redux:

  • Zero boilerplate — no actions, reducers, dispatch, action creators, connect()
  • Built-in selector support — subscribe to only the slice you need
  • Works outside React (useful for non-component code)
  • ~1KB vs Redux Toolkit's ~15KB

Redux Toolkit is still excellent for large teams needing strict conventions, time-travel debugging, and RTK Query for server state.

24
What is TanStack Query (React Query) and why do you need it?Medium

TanStack Query (React Query) manages server state — async data from APIs. It handles caching, background refetching, stale-while-revalidate, pagination, mutations, and optimistic updates.

// Without React Query: manual loading/error/cache state in every component
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => { fetch('/api/users').then(...)  // repeated everywhere

// With React Query:
const { data, isLoading, error } = useQuery({
  queryKey: ['users'],       // cache key
  queryFn: () => fetch('/api/users').then(r => r.json()),
  staleTime: 60_000,         // consider fresh for 1 min
  gcTime: 5 * 60_000,        // keep in cache for 5 min after unmount
});

// Mutation with optimistic update:
const mutation = useMutation({
  mutationFn: (newUser) => fetch('/api/users', { method: 'POST', body: JSON.stringify(newUser) }),
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
});

Separates server state (async, remote, eventually consistent) from client state (local, synchronous). Most apps only need React Query + Zustand — no Redux needed.

25
What is Redux Toolkit and how does it differ from classic Redux?Medium
// Classic Redux: verbose (action types, action creators, switch reducer)
const INCREMENT = 'counter/increment';
const increment = () => ({ type: INCREMENT });
function counterReducer(state = 0, action) {
  switch (action.type) {
    case INCREMENT: return state + 1;
    default: return state;
  }
}

// Redux Toolkit (RTK): concise
import { createSlice, configureStore } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => { state.value += 1; },  // Immer under hood — safe mutation
    incrementBy: (state, action) => { state.value += action.payload; },
  },
});

export const { increment, incrementBy } = counterSlice.actions;
const store = configureStore({ reducer: { counter: counterSlice.reducer } });

RTK includes Immer (safe mutations), RTK Query (data fetching + caching), and Redux DevTools setup. If you use Redux, always use RTK — classic Redux is considered legacy.

26
What is Jotai and the atomic state model?Medium
import { atom, useAtom, useAtomValue } from 'jotai';

// Atoms: tiny independent pieces of state
const countAtom = atom(0);
const nameAtom = atom('Alice');

// Derived atom (computed — like useMemo but for global state):
const doubledAtom = atom(get => get(countAtom) * 2);

// Writable derived atom:
const uppercaseNameAtom = atom(
  get => get(nameAtom).toUpperCase(),
  (get, set, newValue) => set(nameAtom, newValue.toLowerCase())
);

// In component — only re-renders when countAtom changes:
function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const doubled = useAtomValue(doubledAtom);  // read-only
  return <button onClick={() => setCount(c => c + 1)}>{count} x2={doubled}</button>;
}

Jotai is excellent for fine-grained reactivity. Components only re-render when their specific atoms change. No Provider needed at the component level (uses a default store). Great for atomic UI state (modal open/close, selected item, filters).

27
When do you choose local state vs global state?Medium
  • Local state (useState) — used by only one component or its direct children. Form input values, toggle state, accordion open/closed, hover state. Always prefer local state first.
  • Lifted state — when two sibling components need the same data, lift state to their closest common ancestor and pass down as props.
  • Context — theme, locale, current user, auth state. Values that are read widely but update infrequently.
  • Global store (Zustand/Redux) — complex client-side state shared across distant parts of the app: shopping cart, notification system, real-time data, multi-step forms across routes.
  • Server state (React Query) — any async data from an API. Don't put API responses in Redux — React Query handles caching, deduplication, and revalidation.
Most apps are over-globalised. Start with local state, lift when needed, reach for a global store only when lifting becomes impractical.
28
What is Immer and how does it simplify immutable state updates?Medium
// Without Immer: deeply nested immutable update is verbose and error-prone
setState(prev => ({
  ...prev,
  users: prev.users.map(u =>
    u.id === targetId
      ? { ...u, address: { ...u.address, city: 'Mumbai' } }
      : u
  )
}));

// With Immer: write mutating code, Immer produces immutable result
import { produce } from 'immer';

setState(prev => produce(prev, draft => {
  const user = draft.users.find(u => u.id === targetId);
  if (user) user.address.city = 'Mumbai';  // "mutate" the draft
}));

// useImmer hook:
import { useImmer } from 'use-immer';
const [state, updateState] = useImmer(initialState);
updateState(draft => { draft.users[0].city = 'Mumbai'; });

Immer uses JavaScript Proxy to track mutations on a draft object, then produces an immutable updated state. RTK uses Immer internally in createSlice reducers.

29
How do you persist state to localStorage?Medium
// Custom hook: useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch { return initialValue; }
  });

  const setStoredValue = (newValue: T) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  };

  return [value, setStoredValue] as const;
}

// Zustand with persist middleware:
import { persist } from 'zustand/middleware';
const useStore = create(persist(
  (set) => ({ theme: 'dark', setTheme: (t) => set({ theme: t }) }),
  { name: 'app-settings', storage: createJSONStorage(() => localStorage) }
));
localStorage is synchronous and blocks the main thread for large data. For large persisted state, use IndexedDB (idb library). Also: localStorage is not available during SSR — always check typeof window !== 'undefined'.
30
What is the Context + useReducer pattern?Medium
// Lightweight alternative to Redux for medium-complexity apps
const StoreContext = createContext(null);

function storeReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': return { ...state, items: [...state.items, action.item] };
    default: return state;
  }
}

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(storeReducer, { items: [] });
  // Split context to avoid re-rendering dispatch consumers on state change:
  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
}

// Optimisation: split into StateContext + DispatchContext
// Components that only dispatch don't re-render when state changes
Performance Optimisation
31
What is React.memo and when does it help?Medium
// React.memo: skip re-render if props haven't changed (shallow equal)
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
  return items.map(item => <Item key={item.id} item={item} onSelect={onSelect} />);
});

// IMPORTANT: works only if props are stable references!
// Parent re-renders → new function reference each time:
<ExpensiveList
  items={items}
  onSelect={(id) => console.log(id)}  // new function every render → memo useless!
/>

// Fix: stabilise the function with useCallback
const handleSelect = useCallback((id) => console.log(id), []);
<ExpensiveList items={items} onSelect={handleSelect} />

// Custom comparison:
const ExpensiveList = React.memo(List, (prev, next) =>
  prev.items.length === next.items.length  // custom equality
);

Profile before adding React.memo. It adds a comparison cost on every render — only beneficial when the comparison is cheaper than the re-render it prevents.

32
What is code splitting and lazy loading in React?Medium
// Without code splitting: entire app JS in one bundle → slow initial load
// With React.lazy: each lazy-loaded chunk downloaded only when needed

// Route-level splitting (most impactful):
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings  = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<PageSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings"  element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

// Component-level splitting (heavy components: chart libraries, editors):
const RichEditor = lazy(() => import('./components/RichEditor'));
// Shown only when user clicks "edit" — why load 200KB Quill on every page?

// Preload on hover (UX improvement):
const preloadDashboard = () => import('./pages/Dashboard');
<Link to="/dashboard" onMouseEnter={preloadDashboard}>Dashboard</Link>
33
What is virtualisation (windowing) and when do you need it?Medium

Virtualisation renders only the visible rows/items in a long list — not all 10,000 items at once. Unmounted items are replaced by spacer divs that maintain scroll position.

// TanStack Virtual (replaces react-window/react-virtualized):
import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }) {
  const parentRef = useRef(null);
  const rowVirtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,  // estimated row height
  });

  return (
    <div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
      <div style={{ height: rowVirtualizer.getTotalSize() }}>
        {rowVirtualizer.getVirtualItems().map(virtualRow => (
          <div key={virtualRow.key}
               style={{ position:'absolute', top: virtualRow.start, height: virtualRow.size }}>
            {items[virtualRow.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

Use for: lists >500 items, data grids, infinite scroll feeds. Renders only ~20–30 DOM nodes regardless of list size.

34
How do you profile and debug React performance?Hard

Tools:

  • React DevTools Profiler — records renders, shows which component rendered, why it rendered, how long it took. Look for unnecessary re-renders (components that render but output didn't change).
  • React DevTools "Highlight updates" — flashes components when they re-render. Fast way to spot unexpected re-renders.
  • Chrome Performance tab — Flame chart shows main thread blocking, long tasks (>50ms), layout/paint costs.
  • Why Did You Render library — logs to console when a component re-renders with identical props/state.
// why-did-you-render setup:
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, { trackAllPureComponents: true });

// Common causes of re-renders:
// 1. New object/array reference in JSX: <Child data={{}} /> → new ref every render
// 2. New function in JSX: <Child onClick={() => {}} /> → wrap in useCallback
// 3. Context value changing: split large contexts or use useMemo on value
// 4. State too high in tree: move state down closer to where it's used
35
What are React Server Components and how do they change data fetching?Hard

React Server Components (RSC) run exclusively on the server — their code is never sent to the browser. They can directly access databases, file systems, and internal APIs without exposing credentials to the client.

// app/users/page.tsx — Server Component (default in Next.js App Router)
// No 'use client' directive = server component
async function UsersPage() {
  // Direct DB access — no API layer needed, no credentials exposed to client
  const users = await db.query('SELECT * FROM users LIMIT 20');

  return (
    <div>
      {users.map(u => <UserCard key={u.id} user={u} />)}
      <AddUserButton />  {/* client component for interactivity */}
    </div>
  );
}

// 'use client' — runs on client, can use hooks and event handlers
'use client';
function AddUserButton() {
  const [open, setOpen] = useState(false);
  return <button onClick={() => setOpen(true)}>Add User</button>;
}

Benefits: zero JS sent for server-only code, no waterfall API calls (fetch in parallel on server), data co-located with components, smaller client bundle. RSC is the foundation of Next.js App Router.

36
What is image optimisation in React applications?Medium
// Next.js Image component: automatic optimisation
import Image from 'next/image';

// Automatically:
// - Converts to WebP/AVIF (30-50% smaller)
// - Lazy loads images below the fold
// - Prevents layout shift (requires width + height or fill)
// - Serves correct size for the device (srcset)
// - Caches optimised images

<Image
  src="/hero.jpg"
  alt="Hero banner"
  width={1200}
  height={600}
  priority  // preload above-the-fold images (remove lazy loading)
  placeholder="blur"
  blurDataURL={blurDataUrl}  // tiny LQIP shown while loading
/>

// Core Web Vitals affected by images:
// LCP (Largest Contentful Paint): hero image — use priority + preload
// CLS (Cumulative Layout Shift): always specify dimensions to reserve space
37
What are Web Vitals and how do you measure them in React?Medium
  • LCP (Largest Contentful Paint) — time until the largest visible element loads. Target: <2.5s. Fix: preload hero images, optimise server response, avoid render-blocking resources.
  • INP (Interaction to Next Paint) — responsiveness delay from user input to next paint. Replaced FID in 2024. Target: <200ms. Fix: reduce JS on main thread, use useTransition for non-urgent updates.
  • CLS (Cumulative Layout Shift) — unexpected layout movement. Target: <0.1. Fix: always set image dimensions, reserve space for dynamic content (skeleton loaders), avoid inserting content above existing content.
// Measure in Next.js (built-in):
// app/layout.tsx
export function reportWebVitals(metric) {
  // Send to your analytics:
  analytics.track(metric.name, { value: metric.value, id: metric.id });
}

// Or use web-vitals library:
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
38
What is the React Compiler (React Forget) and what problem does it solve?Hard

The React Compiler (previously "React Forget") is a Babel/compiler plugin that automatically memoizes React components and hooks — eliminating the need for manual useMemo, useCallback, and React.memo.

Problem it solves: Developers either forget to memoize (causing unnecessary re-renders) or over-memoize (adding complexity/overhead everywhere). Both are bugs.

// Before React Compiler: you write this
const handleClick = useCallback(() => doSomething(id), [id]);
const memoedValue = useMemo(() => compute(data), [data]);
const Child = React.memo(ChildComponent);

// After React Compiler: write this, compiler adds memoization automatically
const handleClick = () => doSomething(id);
const memoedValue = compute(data);
// Child is automatically memoized if the compiler can prove it's safe

Released in React 19 (2025). Requires your code to follow React rules (no mutation of props, etc.). Gradually adoptable per file/component. Meta uses it in production on Instagram. Enabled in Next.js 15 config.

Next.js, TypeScript & Testing
39
What are the rendering strategies in Next.js (SSR, SSG, ISR, CSR)?Medium
  • SSR (Server-Side Rendering) — HTML generated on the server per request. Fresh data every request. Use for: personalised pages (dashboards), data that changes frequently.
  • SSG (Static Site Generation) — HTML generated at build time. Served as static files from CDN. Fast, no server needed. Use for: marketing pages, blog posts, docs.
  • ISR (Incremental Static Regeneration) — SSG page revalidated in the background after a time interval. Best of both: CDN speed + fresh data. Use for: product pages, news articles.
  • CSR (Client-Side Rendering) — blank HTML, JS fetches data after load. Use for: dashboards behind auth, highly dynamic content where SEO isn't needed.
// Next.js App Router (React Server Components):
// SSR: fetch inside server component (default — no cache)
const data = await fetch('/api/data', { cache: 'no-store' });

// SSG: fetch at build time (static)
const data = await fetch('/api/data', { cache: 'force-cache' });

// ISR: revalidate every 60 seconds
const data = await fetch('/api/data', { next: { revalidate: 60 } });
// Or: export const revalidate = 60; at the segment level
40
What are Next.js Server Actions?Medium
// Server Action: async function marked 'use server' — runs on server
// called directly from client components or forms (no API route needed)

// app/actions.ts
'use server';
export async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  await db.users.create({ data: { name } });
  revalidatePath('/users');  // revalidate cached page
}

// Client component: call server action directly
'use client';
import { createUser } from './actions';

function AddUserForm() {
  return (
    <form action={createUser}>   {/* Next.js wires the POST automatically */}
      <input name="name" />
      <button type="submit">Add</button>
    </form>
  );
}

// Or call imperatively:
const handleClick = async () => {
  await createUser(formData);  // RPC-like call to server
};

Server Actions replace API routes for mutations. They work without JavaScript (progressive enhancement), support optimistic updates via useOptimistic, and are automatically CSRF-protected.

41
What is the Next.js App Router and how does it differ from Pages Router?Medium
FeaturePages RouterApp Router
Default component typeClientServer
Data fetchinggetServerSideProps / getStaticPropsasync component / fetch()
Layouts_app.tsx (global only)Nested layout.tsx files
Loading stateManualloading.tsx (auto Suspense)
Error handlingManualerror.tsx (auto Error Boundary)
Streaming SSRNoYes (Suspense-based)
// App Router file conventions:
app/
  layout.tsx         // shared layout (persistent across navigation)
  loading.tsx        // Suspense fallback
  error.tsx          // Error boundary
  not-found.tsx      // 404
  page.tsx           // route segment
  users/
    [id]/
      page.tsx       // /users/123
42
What are React's TypeScript best practices?Medium
// 1. Type props with interface (extendable) or type (unions/intersections)
interface ButtonProps {
  children: React.ReactNode;  // any renderable content
  onClick?: () => void;
  variant: 'primary' | 'danger';
}

// 2. Generic components
function Select<T extends { id: string; label: string }>({
  options,
  onSelect,
}: {
  options: T[];
  onSelect: (item: T) => void;
}) { /* ... */ }

// 3. Type events properly
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); };
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {};

// 4. useRef with null (DOM ref):
const ref = useRef<HTMLInputElement>(null);
ref.current?.focus();  // optional chain — current is null before mount

// 5. Discriminated unions for component variants:
type AlertProps =
  | { type: 'success'; message: string }
  | { type: 'error'; message: string; onRetry: () => void };
43
What is React Testing Library and how do you write good tests?Medium
import { render, screen, userEvent } from '@testing-library/react';
import { vi } from 'vitest';

// Guiding principle: "Test the way users use your app, not implementation details"

test('submitting a login form calls onLogin with credentials', async () => {
  const user = userEvent.setup();
  const mockLogin = vi.fn();

  render(<LoginForm onLogin={mockLogin} />);

  // Query by accessible role/label (not by className or test-id)
  await user.type(screen.getByLabelText(/email/i), 'test@example.com');
  await user.type(screen.getByLabelText(/password/i), 'password123');
  await user.click(screen.getByRole('button', { name: /sign in/i }));

  expect(mockLogin).toHaveBeenCalledWith({
    email: 'test@example.com',
    password: 'password123',
  });
});

// Query priority (use in this order):
// getByRole → getByLabelText → getByPlaceholderText → getByText
// Avoid: getByTestId (last resort), getByClassName (implementation detail)
44
How do you test async components and API calls?Medium
import { render, screen, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';

// MSW (Mock Service Worker): intercept real fetch calls in tests
const server = setupServer(
  http.get('/api/users', () => {
    return HttpResponse.json([{ id: 1, name: 'Alice' }]);
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('displays users after loading', async () => {
  render(<UserList />);

  // Loading state:
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // Wait for async to resolve:
  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
  });
  expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});

// Test error state:
test('shows error on API failure', async () => {
  server.use(http.get('/api/users', () => new HttpResponse(null, { status: 500 })));
  render(<UserList />);
  await screen.findByText(/error/i);  // findBy = waitFor + getBy
});
45
What is Vitest and how does it compare to Jest for React testing?Medium
AspectJestVitest
BundlerBabel (separate transform)Vite-native (same config)
SpeedSlower (cold start)3-10x faster (ESM + HMR)
TypeScriptNeeds ts-jest or babelNative, no config needed
API compatibilityJest-compatible (drop-in)
// vitest.config.ts:
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/setupTests.ts'],
    globals: true,     // no need to import describe/it/expect
  },
});

// setupTests.ts:
import '@testing-library/jest-dom';

Vitest is the default for new Vite-based React projects. Jest remains common in CRA or older codebases. API is nearly identical — migration is straightforward.

46
What is Storybook and what role does it play in component development?Medium

Storybook is an isolated component development environment. You build and document components in isolation, without needing to run the full app.

// Button.stories.tsx:
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  component: Button,
  tags: ['autodocs'],  // auto-generate docs from JSDoc + prop types
};
export default meta;

export const Primary: StoryObj<typeof Button> = {
  args: { label: 'Click me', variant: 'primary' },
};
export const Disabled: StoryObj<typeof Button> = {
  args: { label: 'Disabled', disabled: true },
};
// Each "story" = one component state = one visual test case

Benefits: design system documentation, visual regression testing (Chromatic), faster development (no app context needed), designer handoff. Storybook 8 (2024) improved performance and native Vite support.

47
What is a design system and how do you implement one with React?Hard

A design system is a shared library of reusable UI components, tokens (colors, spacing, typography), and guidelines that ensure visual and functional consistency across products.

// tokens.ts: design tokens (source of truth)
export const tokens = {
  colors: { primary: '#6366f1', danger: '#ef4444', text: '#1e293b' },
  spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
  fontSizes: { sm: '0.875rem', base: '1rem', lg: '1.125rem' },
} as const;

// Component with variants (using class-variance-authority):
import { cva } from 'class-variance-authority';
const buttonVariants = cva('rounded-lg font-semibold transition-colors', {
  variants: {
    variant: {
      primary: 'bg-indigo-500 text-white hover:bg-indigo-600',
      ghost:   'bg-transparent text-indigo-500 hover:bg-indigo-50',
    },
    size: {
      sm: 'px-3 py-1.5 text-sm',
      md: 'px-4 py-2 text-base',
    },
  },
  defaultVariants: { variant: 'primary', size: 'md' },
});

Popular approaches: Radix UI (unstyled, accessible primitives) + Tailwind CSS, shadcn/ui (copy-paste Radix + Tailwind), Chakra UI, MUI.

48
What is accessibility (a11y) in React and how do you implement it?Medium
// 1. Semantic HTML first:
<button onClick={handleClick}>Save</button>  // NOT <div onClick>
<nav>, <main>, <header>, <article>           // landmark roles

// 2. ARIA when semantics aren't enough:
<div role="alert" aria-live="polite">{errorMessage}</div>
<button aria-label="Close dialog" aria-expanded={isOpen}><XIcon /></button>

// 3. Focus management for modals/dialogs:
// When modal opens → focus first interactive element
// When modal closes → return focus to trigger button
// Trap focus inside modal (use Radix Dialog — it handles this)

// 4. Color contrast: minimum 4.5:1 for normal text (WCAG AA)

// 5. Keyboard navigation: all interactive elements reachable by Tab,
//    activated by Enter/Space

// Tooling:
// eslint-plugin-jsx-a11y: lint for common a11y issues in JSX
// axe DevTools: browser extension + @axe-core/react for test integration
// Radix UI: all primitives are WAI-ARIA compliant out of the box
49
What is the React 19 use hook?Hard
// React 19's new 'use' hook: read resources in render
// (unlike other hooks, can be called conditionally)

import { use, Suspense } from 'react';

// Use a Promise (suspends until resolved):
function UserName({ userPromise }) {
  const user = use(userPromise);  // suspends if not resolved yet
  return <h1>{user.name}</h1>;
}

// Pass a promise from Server Component to Client Component:
// app/page.tsx (Server Component)
export default function Page() {
  const userPromise = fetchUser(1);  // starts fetch, doesn't await
  return (
    <Suspense fallback={<Spinner />}>
      <UserName userPromise={userPromise} />
    </Suspense>
  );
}

// Use Context (conditionally!):
function Component({ show }) {
  if (!show) return null;
  const theme = use(ThemeContext);  // valid — use() can be in conditionals
  return <div className={theme}>...</div>;
}

The use hook is a step toward a more unified async model in React. It works with any Thenable (Promise-like), not just native Promises.

50
How do you architect a large-scale React application?Hard
src/
  app/                  # Next.js App Router pages + layouts
  features/             # Feature-based slices (preferred over type-based)
    auth/
      components/       # components used only by auth feature
      hooks/            # useAuth, useSession
      api/              # API calls for auth
      store/            # auth Zustand slice
      types.ts
    dashboard/
    products/
  shared/               # truly shared across features
    components/         # Button, Modal, Table, Input (design system)
    hooks/              # useDebounce, useLocalStorage
    utils/              # formatCurrency, validateEmail
    types/              # common TypeScript types
    api/                # API client, interceptors

Key architectural decisions:

  • Feature-sliced architecture — co-locate everything a feature needs; avoid cross-feature imports downward
  • Separate server state (React Query) from client state (Zustand) — don't put API data in global store
  • Component variants via CVA or compound components — not prop explosion
  • Absolute imports (@/features/auth) — avoid ../../.. hell
  • Barrel exports (index.ts) for public API of each feature — other features import from the barrel

What to Study Next