🏷️ Get 20% OFF on Yearly & Lifetime plans β€” use code 🏷️ Get 20% OFF on Yearly & Lifetime plans β€” use code 🏷️ Get 20% OFF on Yearly & Lifetime plans β€” use code 🏷️ Get 20% OFF on Yearly & Lifetime plans β€” use code 🏷️ Get 20% OFF on Yearly & Lifetime plans β€” use code 🏷️ Get 20% OFF on Yearly & Lifetime plans β€” use code
← All articles
Interview PrepReact Coding QuestionsPracticeSolutions

React Coding Interview Questions with Solutions (2026)

10 React coding interview questions with tested solutions: counter, forms, todo list, pagination, hooks and a cart, plus what interviewers check.

Kumar AstikΒ· MERN Developer18 min read

React coding interview questions are different from "explain useEffect" questions. Someone gives you a small spec, an editor and a clock, and expects a working component at the end. This guide collects 10 of the React interview coding questions that come up most often. Each one has a complete solution, the reasoning behind it, the follow-up an interviewer is likely to ask, and a practice link.

Every solution below was run against tests (including empty input, rapid clicks, pasted text and unmounting) and type-checked in strict TypeScript, so you can copy it into your own editor and rely on it. These reactjs coding questions are also available as practice challenges with hidden tests, so you can check your own version instead of only reading mine.

The 10 React Coding Interview Questions at a Glance

  • 1. Counter with increment, decrement and reset (useState)
  • 2. Textarea with a live character counter and a maximum length (controlled input)
  • 3. Accordion where only one section is open at a time (state design)
  • 4. Todo list with add, complete and delete (immutable updates)
  • 5. Traffic light that cycles on a timer (useEffect cleanup)
  • 6. Fetching data with loading, error and empty states (async and AbortController)
  • 7. A useLocalStorage custom hook (lazy state and persistence)
  • 8. Pagination with numbered buttons (derived state)
  • 9. Star rating with hover preview (UI state and accessibility)
  • 10. Shopping cart with useReducer (reducers and derived totals)
  • Bonus: fix a stale closure bug in a counter

How to Use These React Coding Questions

Don't read the solution first. Open a blank editor, set a 20 to 30 minute timer, and build the component yourself. Then compare it with the solution below and note what you missed. It is usually an edge case, not the main logic. A week later, redo the ones you struggled with without looking at the code. That second attempt is what shows whether the pattern has actually stuck.

What Interviewers Check in a React Coding Interview

Working code is only part of the score. In most React coding rounds, interviewers also look at:

  • Component structure: is the code split into sensible pieces, or is everything in one large component
  • State design: is state minimal and in the right place, with no values stored that could be calculated
  • Edge cases: empty states, loading and error states, pasted input, rapid clicks, and a component unmounting mid-request
  • Correct use of hooks: dependency arrays, cleanup functions, and functional state updates
  • Communication: asking clarifying questions and explaining your choices as you code

For the full rubric, timing advice and a worked example, read the machine coding round guide. Read the machine coding round guide β†’

Beginner React Coding Questions

1. Build a counter with increment, decrement and reset

The task: build a Counter component that shows a number, with buttons to increment, decrement and reset it to the starting value.

import { useState } from "react";

function Counter({ initial = 0 }: { initial?: number }) {
  const [count, setCount] = useState(initial);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <button onClick={() => setCount((c) => c - 1)}>Decrement</button>
      <button onClick={() => setCount(initial)}>Reset</button>
    </div>
  );
}

The key detail is the functional update, setCount((c) => c + 1). It always works from the latest state, so rapid clicks and batched updates never lose a count. Reset uses the initial prop instead of a hard-coded 0, which is the part candidates most often get wrong. Interviewers use this question as a warm-up to check your basics: state, event handlers and updating state from its previous value. A likely follow-up: how would you stop the count going below zero, or add a step size prop?

Practice this question against real tests. Try the Counter with reset challenge β†’

2. Build a textarea with a live character counter and a maximum length

The task: build a textarea with a counter that shows how many characters are used. The counter changes color when the limit is reached, and pasted text must be cut off at the limit.

import { useState } from "react";

function TextareaWithCounter({ maxLength = 100 }: { maxLength?: number }) {
  const [text, setText] = useState("");
  const atLimit = text.length >= maxLength;

  return (
    <div>
      <textarea
        aria-label="Message"
        value={text}
        onChange={(e) => setText(e.target.value.slice(0, maxLength))}
      />
      <p style={{ color: atLimit ? "red" : "inherit" }}>
        {text.length} / {maxLength}
      </p>
    </div>
  );
}

Cutting the value in onChange with e.target.value.slice(0, maxLength) handles typing and pasting in one place, so the state can never go over the limit. The counter is calculated from text.length instead of being kept in a second piece of state, which avoids the two values getting out of sync. You can also add the native maxLength attribute to the textarea, but cutting the value in the handler keeps your logic correct even when the value is set from code. A likely follow-up: count words instead, or announce the remaining characters to screen readers with aria-live.

Practice the controlled textarea with a character counter. Try the React textarea challenge β†’

3. Build an accordion where only one section is open at a time

The task: build an accordion. Clicking a header opens its section and closes any other open one. Clicking the open header closes it.

import { useState } from "react";

type Item = { id: string; title: string; content: string };

function Accordion({ items }: { items: Item[] }) {
  const [openId, setOpenId] = useState<string | null>(null);

  return (
    <div>
      {items.map((item) => {
        const isOpen = item.id === openId;
        return (
          <div key={item.id}>
            <button
              aria-expanded={isOpen}
              onClick={() => setOpenId(isOpen ? null : item.id)}
            >
              {item.title}
            </button>
            {isOpen && <p>{item.content}</p>}
          </div>
        );
      })}
    </div>
  );
}

Store which section is open (openId) instead of an open or closed flag for each section. One piece of state enforces the "only one open" rule automatically, and the toggle is a single line. The aria-expanded attribute tells assistive technology whether each header's section is open, which interviewers often look for. A likely follow-up: allow several sections open at once (use a Set of ids) or animate the panel.

Practice this state-design question with hidden tests. Try the Accordion challenge β†’

Intermediate React Coding Questions

4. Build a todo list with add, complete and delete

The task: build a todo list where you can add tasks, mark them done and delete them. Ignore empty tasks, and show a message when there are no tasks.

import { useRef, useState, type FormEvent } from "react";

type Todo = { id: number; text: string; done: boolean };

function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [text, setText] = useState("");
  const nextId = useRef(1);

  const addTodo = (e: FormEvent) => {
    e.preventDefault();
    const trimmed = text.trim();
    if (!trimmed) return;
    const id = nextId.current++;
    setTodos((prev) => [...prev, { id, text: trimmed, done: false }]);
    setText("");
  };

  const toggleTodo = (id: number) =>
    setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));

  const deleteTodo = (id: number) =>
    setTodos((prev) => prev.filter((t) => t.id !== id));

  return (
    <div>
      <form onSubmit={addTodo}>
        <input
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="Add a task"
        />
        <button type="submit">Add</button>
      </form>

      {todos.length === 0 && <p>No tasks yet.</p>}

      <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)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Every update creates a new array with map or filter and never changes the old one. Mutating the existing array (for example with push) is the most common mistake here: React sees the same reference and the screen doesn't update. The id is created outside the state updater, because updater functions must be pure and React may call them twice in development. Using the form's onSubmit means the Enter key works without extra code. A likely follow-up: save the todos with the useLocalStorage hook below, add editing, or add an all, active and done filter.

Practice the classic todo list with hidden tests. Try the Todo List challenge β†’

5. Build a traffic light that cycles on a timer

The task: build a traffic light that goes red, green, yellow and back to red forever, with a different duration for each color.

import { useEffect, useState } from "react";

const STEPS = [
  { color: "red", duration: 4000 },
  { color: "green", duration: 3000 },
  { color: "yellow", duration: 1000 },
];

function TrafficLight() {
  const [index, setIndex] = useState(0);

  useEffect(() => {
    const id = setTimeout(
      () => setIndex((i) => (i + 1) % STEPS.length),
      STEPS[index].duration,
    );
    return () => clearTimeout(id); // cancel the pending timer on every change and on unmount
  }, [index]);

  return (
    <div>
      {STEPS.map((step, i) => (
        <div
          key={step.color}
          data-testid={step.color}
          style={{
            width: 40,
            height: 40,
            borderRadius: "50%",
            background: step.color,
            opacity: i === index ? 1 : 0.2,
          }}
        />
      ))}
    </div>
  );
}

Keep the sequence as data and store only the current index. The effect schedules one timeout for the current step and returns a cleanup function that cancels it. A new timer is created each time the index changes, and nothing is left running after the component unmounts. Using setTimeout, not setInterval, is what lets each color have its own duration. Interviewers check for exactly this: the cleanup function and the dependency array. A likely follow-up: add a pause and resume button, or let the durations come from props.

Practice effects and timer cleanup against real tests. Try the Traffic Light challenge β†’

6. Fetch data with loading, error and empty states

The task: fetch a list of users from an API and show a loading message, an error message, an empty state, or the list.

import { useEffect, useState } from "react";

type FetchState<T> = { data: T | null; loading: boolean; error: string | null };

function useFetch<T>(url: string): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    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(`Request failed with status ${res.status}`);
        return res.json() as Promise<T>;
      })
      .then((data) => setState({ data, loading: false, error: null }))
      .catch((err: Error) => {
        if (err.name === "AbortError") return; // a newer request replaced this one
        setState({ data: null, loading: false, error: err.message });
      });

    return () => controller.abort();
  }, [url]);

  return state;
}

function UserList() {
  const { data, loading, error } = useFetch<{ id: number; name: string }[]>("/api/users");

  if (loading) return <p>Loading…</p>;
  if (error) return <p role="alert">{error}</p>;
  if (!data || data.length === 0) return <p>No users found.</p>;

  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

The hook keeps data, loading and error together, and the component renders each case in order. The AbortController cancels the request when the component unmounts or the URL changes, so an old, slow response can't overwrite a newer one. The AbortError is ignored on purpose because it isn't a real failure. Checking res.ok matters because fetch doesn't reject on a 404 or 500 by itself. A likely follow-up: add a retry button, add a cache, or explain how a library like React Query would change this.

Practice a data-fetching problem with loading and error states. Try the Weather Dashboard challenge β†’

7. Write a useLocalStorage custom hook

The task: write a custom hook, useLocalStorage(key, initialValue), that works like useState but saves the value in localStorage and restores it on the next visit.

import { useEffect, useState } from "react";

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const stored = window.localStorage.getItem(key);
      return stored !== null ? (JSON.parse(stored) as T) : initialValue;
    } catch {
      return initialValue; // invalid JSON or storage blocked
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch {
      // storage can be full or blocked; ignore the error
    }
  }, [key, value]);

  return [value, setValue] as const;
}

function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");

  return (
    <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
      Current theme: {theme}
    </button>
  );
}

The function passed to useState (lazy initialization) reads localStorage only on the first render, not on every render. Both the read and the write are wrapped in try/catch, because stored JSON can be invalid and storage can be blocked or full. The effect saves the value whenever it changes, and returning [value, setValue] as const keeps the tuple's types correct. A likely follow-up: window doesn't exist when rendering on a server, so a server-rendered app needs a guard such as typeof window !== "undefined". You may also be asked how to keep several tabs in sync using the storage event.

Practice writing this custom hook against hidden tests. Try the useLocalStorage hook challenge β†’

8. Build pagination with numbered buttons

The task: given a list of items, show 5 per page with numbered page buttons and Prev and Next buttons. Prev and Next must be disabled on the first and last page.

import { useState } from "react";

function Pagination({ items, pageSize = 5 }: { items: string[]; pageSize?: number }) {
  const [page, setPage] = useState(1);

  const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
  const currentPage = Math.min(page, totalPages); // stays valid if the list shrinks
  const start = (currentPage - 1) * pageSize;
  const visibleItems = items.slice(start, start + pageSize);

  return (
    <div>
      <ul>
        {visibleItems.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>

      <button onClick={() => setPage(currentPage - 1)} disabled={currentPage === 1}>
        Prev
      </button>

      {Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => (
        <button
          key={n}
          onClick={() => setPage(n)}
          aria-current={n === currentPage ? "page" : undefined}
        >
          {n}
        </button>
      ))}

      <button onClick={() => setPage(currentPage + 1)} disabled={currentPage === totalPages}>
        Next
      </button>
    </div>
  );
}

Nothing about the visible items is stored. totalPages and the visible slice are calculated from items, pageSize and page on every render. currentPage = Math.min(page, totalPages) keeps the page valid if the list gets shorter, and Math.max(1, ...) handles an empty list. The Prev and Next buttons are disabled at the boundaries, and aria-current marks the active page for screen readers. A likely follow-up: show only a window of page numbers (1 ... 4 5 6 ... 20), or reset to page one when a filter changes.

Practice this classic UI component against hidden tests. Try the React Pagination challenge β†’

9. Build a star rating with hover preview

The task: build a star rating component. Clicking a star sets the rating, hovering previews a rating without saving it, and a callback reports the chosen value.

import { useState } from "react";

function StarRating({
  max = 5,
  onChange,
}: {
  max?: number;
  onChange?: (value: number) => void;
}) {
  const [rating, setRating] = useState(0);
  const [hovered, setHovered] = useState(0);
  const active = hovered || rating; // hover preview wins while the mouse is over a star

  return (
    <div role="radiogroup" aria-label="Rating">
      {Array.from({ length: max }, (_, i) => i + 1).map((star) => (
        <button
          key={star}
          type="button"
          role="radio"
          aria-checked={rating === star}
          aria-label={`${star} star${star > 1 ? "s" : ""}`}
          onClick={() => {
            setRating(star);
            onChange?.(star);
          }}
          onMouseEnter={() => setHovered(star)}
          onMouseLeave={() => setHovered(0)}
        >
          {star <= active ? "β˜…" : "β˜†"}
        </button>
      ))}
    </div>
  );
}

There are two pieces of state: the saved rating and the hovered star. The displayed value is hovered || rating, so hovering previews a rating without changing the saved one, and moving the mouse away restores it. The radiogroup and radio roles with aria-checked make the component understandable to assistive technology, and real buttons can be reached with Tab and activated from the keyboard. A likely follow-up: support half stars, a read-only mode, or arrow-key navigation.

Practice this interactive component with hidden tests. Try the Star Rating challenge β†’

Advanced React Coding Questions

10. Build a shopping cart with useReducer

The task: build a shopping cart where you can add products, increase and decrease quantities (removing an item when its quantity reaches zero), remove items, and see the total.

import { useReducer } from "react";

type CartItem = { id: string; name: string; price: number; qty: number };

type Action =
  | { type: "add"; item: Omit<CartItem, "qty"> }
  | { type: "increment"; id: string }
  | { type: "decrement"; id: string }
  | { type: "remove"; id: string };

function cartReducer(state: CartItem[], action: Action): CartItem[] {
  switch (action.type) {
    case "add": {
      const exists = state.some((i) => i.id === action.item.id);
      return exists
        ? state.map((i) => (i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i))
        : [...state, { ...action.item, qty: 1 }];
    }
    case "increment":
      return state.map((i) => (i.id === action.id ? { ...i, qty: i.qty + 1 } : i));
    case "decrement":
      return state
        .map((i) => (i.id === action.id ? { ...i, qty: i.qty - 1 } : i))
        .filter((i) => i.qty > 0); // dropping to zero removes the item
    case "remove":
      return state.filter((i) => i.id !== action.id);
  }
}

const PRODUCTS = [
  { id: "p1", name: "Keyboard", price: 50 },
  { id: "p2", name: "Mouse", price: 25 },
];

function Cart() {
  const [items, dispatch] = useReducer(cartReducer, []);
  const total = items.reduce((sum, i) => sum + i.price * i.qty, 0); // derived, not stored

  return (
    <div>
      {PRODUCTS.map((p) => (
        <button key={p.id} onClick={() => dispatch({ type: "add", item: p })}>
          Add {p.name}
        </button>
      ))}

      {items.length === 0 && <p>Your cart is empty.</p>}

      <ul>
        {items.map((i) => (
          <li key={i.id}>
            {i.name} Γ— {i.qty} β€” ${i.price * i.qty}
            <button onClick={() => dispatch({ type: "increment", id: i.id })}>+</button>
            <button onClick={() => dispatch({ type: "decrement", id: i.id })}>-</button>
            <button onClick={() => dispatch({ type: "remove", id: i.id })}>Remove</button>
          </li>
        ))}
      </ul>

      <p>Total: ${total}</p>
    </div>
  );
}

A reducer fits because several related actions change the same piece of state. cartReducer is a pure function: it returns new arrays and objects and never changes the old ones, which makes it easy to test on its own. The total is calculated while rendering instead of being stored, so it can never go out of date. Decrement uses filter to drop an item when its quantity reaches zero. A likely follow-up: save the cart, add discount codes, or move the reducer into context so other components can use it.

Practice state management with the shopping cart problem. Try the Shopping Cart challenge β†’

Bonus: fix this stale closure bug

Interviewers also hand you broken code. This counter should go up every second, but it stops at 1. Why, and how do you fix it?

// Broken: the interval keeps using the count from the first render
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, []);

// Fixed: the functional update reads the latest value
useEffect(() => {
  const id = setInterval(() => setCount((c) => c + 1), 1000);
  return () => clearInterval(id);
}, []);

The effect runs only once, so the interval callback keeps the count from the first render, which is 0. Every tick calls setCount(0 + 1), and the number never gets past 1. The functional update setCount((c) => c + 1) reads the latest value instead, so it works with an empty dependency array. The other valid fix is adding count to the dependency array, but that recreates the interval on every tick.

Practice finding and fixing this bug against real tests. Try the stale closure debug challenge β†’

"Interviewers rarely remember the component that worked. They remember the one that handled the empty state, the pasted text and the unmount."

Common Mistakes in React Coding Interviews

  • Mutating state (push, splice or assigning to a property) instead of creating a new array or object
  • Storing values you can calculate, such as totals, counts and page slices, in extra state
  • Forgetting the cleanup function in useEffect for timers, listeners and requests
  • Skipping empty, loading and error states because the happy path already works
  • Starting to type before asking clarifying questions about the requirements
  • Going silent while coding instead of explaining what you're building and why

Almost none of these are knowledge gaps. They are habits, which is why timed practice with tests that check edge cases helps more than reading more solutions.

More React Coding Questions to Practice Next

Once these ten feel automatic, move on to questions that combine several ideas. A debounced search box is the classic one, and our machine coding guide walks through a complete solution. A copy-to-clipboard button with a "Copied!" state is another common small task.

Understand when to debounce and when to throttle, with code for both. Read debounce vs throttle in React β†’

Build a reusable copy-to-clipboard hook, including the edge cases. Read the React copy to clipboard guide β†’

Coding questions are only half of most interview loops. You'll also be asked to explain concepts out loud, such as stale closures, keys and re-renders.

Prepare the explanation side with short answers, analogies and code. Browse the React interview questions and answers β†’

Preparing for a 3 to 5+ year role? See what changes at the experienced level. Read the React interview questions for experienced developers β†’

The Practical Takeaway

React coding interview questions reward the same few habits: update state without mutating it, keep state minimal, clean up effects, and handle the empty, loading and error cases before the interviewer asks. You don't need hundreds of questions. Ten patterns, practiced until you can write each one cleanly under a timer, cover most of what the coding round asks.

Reading a solution only shows you that it works. Writing it yourself, against tests that check the edge cases, shows you that you can.

Ready to try these as timed challenges with hidden tests? Browse all React coding challenges

Frequently asked questions

What are the most common React coding interview questions?

The same few categories come up again and again: small state components (a counter, an accordion, a todo list), form handling (controlled inputs, validation, character counters), timers and effects (a traffic light, a countdown), data fetching with loading and error states, reusable custom hooks (useLocalStorage, useDebounce), and list features like pagination, star ratings or a shopping cart.

How do I practice React coding interview questions?

Solve each one from a blank editor before reading any solution, then run it against real tests that check edge cases such as empty input, rapid clicks and unmounting. Time yourself, and repeat the problems you struggled with a week later without looking at your old code.

How long do I get to solve a React coding question in an interview?

It varies by company. A small component often gets roughly 30 to 45 minutes, while a larger machine coding round is usually 60 to 90 minutes. Ask about the time limit and the evaluation criteria at the start.

Can I use Google, documentation or AI in a React coding round?

It depends on the company and the round, so ask before you begin and follow the rules you are given. Some live rounds let you look up documentation, while online assessments usually don't allow outside help. Practice without looking things up so you don't depend on it.

Should I write React coding interview solutions in TypeScript or JavaScript?

Use whatever the interviewer provides or asks for. If you get a choice, use the one you can write fastest and most cleanly. TypeScript can show care about data shapes, but a slow, error-prone TypeScript attempt is worse than clean JavaScript.

What is the difference between React coding questions and React conceptual interview questions?

Coding questions ask you to build or fix something that runs, such as a pagination component, and are judged by what it does. Conceptual questions ask you to explain ideas like stale closures or keys, and are judged by your reasoning. Most interview loops include both, so prepare for each separately.

About the author
Kumar Astik β€” MERN Developer
Keep reading