Coding Interviews

React JS Interview Questions: Practical Prep Guide

Prepare for react js interview questions with clear explanations, practical exercises, common pitfalls, and a focused plan for technical interviews.

The Stealth Interview Team13 min read
React JS Interview Questions: Practical Prep Guide

React JS interview questions test more than API recall. You need to explain JavaScript behavior, model state correctly, write a working component, debug unfamiliar code, and defend design trade-offs. A useful preparation plan practices those skills together instead of memorizing isolated answers.

What React JS Interviews Usually Evaluate#

A React technical interview usually separates into JavaScript fundamentals, React concepts, coding, debugging, and design.

JavaScript fundamentals#

React code still follows JavaScript rules. Be ready to reason about:

  • Closures and lexical scope
  • Object and array immutability
  • Promises, async functions, and event-loop behavior
  • Reference equality
  • Array methods such as map, filter, and reduce
  • Modules and imports
  • Destructuring and default values

Weak JavaScript reasoning often appears as a React mistake. A stale closure, for example, is not unique to React. React makes the closure visible because an effect or callback can keep an older value.

React concepts#

You should explain how data moves through a component tree. That includes props, state, rendering, effects, context, refs, and component identity.

Do not stop at definitions. State what problem each feature solves and what can go wrong when you use it unnecessarily.

Hands-on coding#

React coding exercises often start with a small interface:

  • A searchable list
  • A form with validation
  • A modal
  • Tabs or an accordion
  • Data fetching with loading and error states
  • A reusable input or table component

The interviewer may add requirements after the first version works. Keep the initial design simple enough to change.

Debugging#

You may receive code that renders too often, shows stale data, loses input state, or loops indefinitely. Your job is to form a hypothesis and verify it.

Avoid rewriting everything. Isolate the fault first.

Architecture discussions#

A React component design conversation tests your decisions more than your syntax. You may need to choose where state belongs, define component boundaries, or compare context with an external store.

Expectations also change with scope:

  • Junior conversations tend to emphasize correct rendering, props, state, events, forms, and basic effects.
  • Mid-level conversations add reusable APIs, asynchronous state, testing boundaries, performance diagnosis, and maintainability.
  • Senior conversations spend more time on ownership, migration costs, failure modes, team constraints, accessibility, and operational trade-offs.

Before answering, clarify the task:

  1. What data enters the component?
  2. Which interactions change it?
  3. Is the data local, shared, or server-owned?
  4. What should happen during loading, failure, and empty states?
  5. Are accessibility or browser constraints part of the requirement?
  6. Does the interviewer want production-ready code or a focused sketch?

Say your assumptions aloud. That gives the interviewer a chance to correct them before you build the wrong thing.

The React Concepts You Need to Explain Clearly#

Strong React JS interview questions ask what a concept solves, when you would use it, and how it fails.

ConceptWhat it solvesWhen to use itCommon misuse
ComponentsDivide an interface into understandable unitsWhen a piece has distinct behavior, presentation, or reuse valueSplitting every wrapper into a separate component
PropsPass data and callbacks from a parentFor explicit parent-to-child communicationCopying props into state without a synchronization requirement
StatePreserve component-owned data between rendersWhen a value changes over time and affects renderingStoring values that can be calculated during render
ReconciliationUpdates the rendered tree from a new React element treeIt happens as React processes rendersTreating rendering as a direct DOM mutation sequence
KeysPreserve item identity among siblingsFor dynamic lists and reordered collectionsUsing an array index when item order can change
Controlled inputsMake React state the source of truth for form valuesWhen validation, formatting, or coordinated behavior needs the current valueAdding state when an uncontrolled input would be simpler
ContextShare a value across a subtreeFor broadly needed values such as themes or authenticated session dataPutting frequently changing unrelated state into one context
RefsHold a mutable value or access an imperative object without causing a renderFor focus, measurement, timers, or integration with non-React APIsUsing refs to hide state that should update the interface
Error boundariesReplace a failed descendant tree with fallback UIAround meaningful failure boundariesExpecting them to handle every event-handler or asynchronous error

Reconciliation is often explained too vaguely. Focus on identity. If an element keeps the same type and position, React can usually preserve its associated state. A changed key can tell React that an item is a different instance.

Keys therefore need to be stable and tied to domain identity:

JavaScript
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

Error boundaries need one qualification. A class component can implement the error-boundary lifecycle methods. Function components do not become error boundaries merely by wrapping descendants in try and catch. Frameworks and libraries may provide their own boundary APIs.

This guide focuses on preparation rather than repeating a question bank. Browse the blog index for related worked-answer material.

Hooks: Reasoning Beyond Syntax#

Hooks questions test whether you understand render-time values, closures, dependencies, and cleanup.

useState#

useState stores data owned by a component. Use a functional update when the next value depends on the previous one:

JavaScript
setCount((current) => current + 1);

This avoids depending on the value captured by the current callback.

A likely follow-up is: Should this value be state at all? If you can derive it from current props and state during render, storing another copy creates synchronization work.

useEffect#

useEffect synchronizes a component with something outside React. Examples include network requests, subscriptions, timers, and imperative browser APIs.

The dependency array describes the reactive values used by the effect. It is not a list of events that should “trigger” the effect.

JavaScript
useEffect(() => {
  document.title = `Results for ${query}`;
}, [query]);

Follow-up prompts include:

  • What external system requires this effect?
  • Can you calculate the value during render instead?
  • What happens when query changes quickly?
  • Does the effect need cleanup?

useMemo#

useMemo caches a calculated value between renders when its dependencies remain unchanged. Use it when you have identified expensive repeated work or need stable identity for a specific reason.

Do not add it to every calculation. Memoization adds dependency management and retains a cached value.

An interviewer may ask: How would you prove this calculation is expensive enough to memoize? A strong answer starts with profiling rather than intuition.

useCallback#

useCallback preserves a function identity between renders. It can help when that identity matters to a memoized child or another hook.

It does not prevent the function from being created in some magical global sense, and it does not automatically make a component faster.

A useful follow-up is: Which consumer benefits from the stable identity? If you cannot identify one, the callback may not need memoization.

useRef#

useRef stores a mutable value across renders without scheduling another render. It works for DOM nodes, timer IDs, previous values, and request identifiers.

Ask: Should a change to this value update the screen? If yes, state is usually a better fit.

Custom hooks#

A custom hook extracts reusable stateful behavior. It does not create shared state by itself. Each call gets its own hook state unless the hook connects to a shared external source.

Good custom hooks expose a small API and hide lifecycle details. They should not obscure ownership.

Stale closures and cleanup#

Each render produces its own values and functions. A delayed callback can retain values from the render that created it.

You can address stale closures by:

  • Including the correct effect dependencies
  • Using functional state updates
  • Recreating a subscription when its inputs change
  • Keeping a mutable latest value in a ref when that behavior is intentional

Cleanup should reverse the setup. Remove listeners, clear timers, unsubscribe, or abort pending requests. Be prepared to explain why cleanup must work even when setup and cleanup occur more than once during development checks.

Worked Exercise: Build a Debounced Search Component#

A debounced search component combines controlled input, asynchronous effects, cleanup, race handling, and accessible status messages.

The component below waits after the latest keystroke before requesting results. It aborts obsolete work and also checks request identity before committing a response.

JavaScript

  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [status, setStatus] = useState("idle");
  const [error, setError] = useState("");
  const latestRequest = useRef(0);

  useEffect(() => {
    const normalizedQuery = query.trim();

    if (!normalizedQuery) {
      setResults([]);
      setStatus("idle");
      setError("");
      return;
    }

    const controller = new AbortController();

    const timerId = setTimeout(async () => {
      const requestId = latestRequest.current + 1;
      latestRequest.current = requestId;

      setStatus("loading");
      setError("");

      try {
        const params = new URLSearchParams({
          q: normalizedQuery,
        });

        const response = await fetch(`/api/search?${params}`, {
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error("Search request failed");
        }

        const data = await response.json();

        if (requestId === latestRequest.current) {
          setResults(data.results ?? []);
          setStatus("success");
        }
      } catch (requestError) {
        if (requestError.name === "AbortError") {
          return;
        }

        if (requestId === latestRequest.current) {
          setResults([]);
          setError("Unable to load results.");
          setStatus("error");
        }
      }
    }, 400);

    return () => {
      clearTimeout(timerId);
      controller.abort();
    };
  }, [query]);

  return (
    <section aria-labelledby="search-heading">
      <h2 id="search-heading">Search documentation</h2>

      <label htmlFor="search-query">Search terms</label>
      <input
        id="search-query"
        type="search"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        autoComplete="off"
      />

      <p role="status" aria-live="polite">
        {status === "loading" && "Loading results…"}
        {status === "error" && error}
        {status === "success" &&
          `${results.length} result${results.length === 1 ? "" : "s"}`}
      </p>

      {status === "success" && results.length === 0 && (
        <p>No matching results.</p>
      )}

      <ul>
        {results.map((result) => (
          <li key={result.id}>
            <a href={result.url}>{result.title}</a>
          </li>
        ))}
      </ul>
    </section>
  );
}

Explain the design step by step#

State ownership: The component owns the query and request presentation state because both directly control its interface. If another component needed the same query, you could lift it to their closest shared owner.

Debouncing: Every query change schedules a timer. Cleanup clears the previous timer, so typing again replaces pending work.

Request cleanup: Once a request begins, cleanup aborts it when the query changes or the component unmounts.

Race protection: Aborting is useful, but request identity provides an additional guard. Only the latest request can update results, status, or errors.

Error handling: Aborts are expected control flow here. Other failures produce an error state and remove obsolete results.

Accessibility: The input has a visible label. The status region announces loading, error, and result changes without moving focus. The empty state is distinct from failure.

Complexity#

Let r be the number of rendered results.

  • Updating the query and managing the timer is constant work aside from React rendering.
  • The component keeps at most one debounce timer for its current effect.
  • It intends to keep one current request, while request identity prevents an older completion from changing visible state.
  • Rendering the result list takes O(r) time.
  • Stored result data and rendered list output take O(r) space.

Useful follow-ups include:

  • Cache results by normalized query.
  • Add pagination and merge pages without duplicate items.
  • Extract fetching and race handling into useDebouncedSearch.
  • Preserve the query in the URL.
  • Add keyboard navigation for a combobox-style result panel.
  • Separate urgent input updates from slower result rendering.

Do not implement every extension immediately. Explain how each requirement changes state ownership and the component API first.

How to Approach React Debugging Questions#

Use the same debugging sequence each time: reproduce, isolate, inspect, verify timing, and apply the smallest correction.

Unstable keys#

Symptom: Editing or reordering a list moves local input state to the wrong row.

Inspection: Check whether list items use their array position as the key.

Correction: Use a stable item identifier.

Explain it aloud like this:

The row state appears attached to position rather than item identity. I would reproduce it by editing one row and reordering the list. Then I would replace the positional key with the item’s stable ID and confirm that the state follows the item.

Direct state mutation#

This update mutates the existing object:

JavaScript
user.name = nextName;
setUser(user);

Use a new object instead:

JavaScript
setUser((current) => ({
  ...current,
  name: nextName,
}));

Your explanation should connect mutation to reference identity. React and memoized consumers cannot reliably reason about a change when you reuse and mutate the existing object.

Infinite effects#

A common loop looks like this:

JavaScript
useEffect(() => {
  setOptions({ sort: "name" });
}, [options]);

The effect creates a new object. That changes options, which runs the effect again.

Ask why options needs to be state. If it is constant or derived, remove the effect and calculate it directly. Do not suppress the dependency warning while leaving the feedback loop intact.

Stale closures#

Suppose an interval repeatedly uses the initial count:

JavaScript
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);

  return () => clearInterval(id);
}, []);

Use the previous state instead:

JavaScript
useEffect(() => {
  const id = setInterval(() => {
    setCount((current) => current + 1);
  }, 1000);

  return () => clearInterval(id);
}, []);

State your reasoning before editing: the interval callback closes over the first render’s count. The functional update removes its need to read that captured value.

Component Design and State Management Trade-Offs#

A strong design answer identifies ownership, update frequency, consumers, and failure modes before choosing a state tool.

  • Local state fits data used by one component or a small subtree.
  • Lifted state fits data that coordinates sibling components.
  • Context distributes a value through a subtree without passing it through every intermediate component.
  • Reducers help when related transitions are easier to describe as explicit actions.
  • External stores can support state shared across distant areas or state managed outside React.

Do not choose based only on application size. Ask:

  • Who reads the state?
  • Who writes it?
  • How often does it change?
  • Does it need persistence?
  • Is it server data or client-owned interface state?
  • Can unrelated consumers subscribe independently?
  • What should happen if an update fails?

Prop drilling is not automatically a defect. A few explicit props can make dependencies easy to see. Context becomes useful when many intermediate components pass data they do not otherwise use.

Avoid duplicated derived state:

JavaScript
const visibleItems = items.filter((item) =>
  item.name.includes(query)
);

You usually do not need to store visibleItems separately. Calculate it from items and query, then memoize only if measurement shows that calculation is costly.

For reusable component APIs, prefer clear composition over a long collection of interdependent flags. Explain what the component owns, what callers can control, and how defaults work.

When there is no single correct design, compare options directly. Name your assumptions. Pick one approach for the current constraints, then describe the condition that would make you reconsider it.

React Performance Questions Without Guesswork#

A React performance interview should begin with evidence about where time is spent.

First distinguish the problem:

  • Frequent rendering: A component renders more often than expected.
  • Expensive rendering: A necessary render performs costly calculation or creates a large tree.
  • Network latency: The interface waits for remote data.
  • Main-thread work: Parsing, formatting, or unrelated JavaScript blocks interaction.
  • Large lists: The browser creates and lays out more elements than the user can see.

These cases need different fixes.

React.memo can skip rendering a component when its props compare as unchanged. It helps only when skipped work matters and props remain stable.

useMemo caches a calculated value. useCallback caches function identity. Neither should be added without identifying the work or identity comparison it supports.

List virtualization limits rendered rows to a visible window. It addresses DOM and rendering cost for large collections, but adds complexity around measurement, scrolling, focus, and accessibility.

Code splitting delays loading code until it is needed. It can improve initial loading behavior, but does not make an expensive mounted component cheap to render.

Use profiling tools to identify expensive commits and components. Then change one thing and profile again. “This avoids re-renders” is incomplete. State which render is avoided, why it is expensive, and what new complexity the optimization introduces.

A Focused React Interview Preparation Plan#

Effective frontend interview preparation moves from language behavior to implementation, then debugging and design.

  1. Review JavaScript first. Practice closures, promises, immutable updates, reference equality, and array transformations.
  2. Explain React’s model. Cover components, props, state, identity, keys, controlled inputs, refs, context, and boundaries.
  3. Practice hooks through scenarios. Explain dependencies, cleanup, stale closures, and why an effect exists.
  4. Build small components. Work on forms, lists, asynchronous search, modals, and reusable controls.
  5. Debug intentionally broken examples. Fix one defect at a time and narrate your hypothesis.
  6. Discuss component design. Decide where state belongs and compare local state, context, reducers, and stores.
  7. Measure performance cases. Separate render cost, network delay, and large-DOM problems.
  8. Practice follow-ups. Add cancellation, caching, pagination, validation, keyboard support, or a reusable hook.

Pair React practice with general coding work. The LeetCode pattern reference organizes common problem shapes, while the NeetCode 150 list provides a structured set for broader coding interview preparation.

For each exercise, practice two outputs: working code and a spoken explanation. State the requirements, choose state ownership, describe edge cases, give complexity, and name the trade-off you accepted. That is closer to the real interview than reciting a definition of useEffect.

Frequently asked questions

What topics should I study for a React JS interview?
Study JavaScript fundamentals, React concepts, hooks, hands-on component coding, debugging, state management, component design, accessibility, and performance diagnosis.
How should I explain useEffect in a React interview?
Explain that useEffect synchronizes a component with an external system such as a network request, subscription, timer, or browser API. Describe its reactive dependencies, cleanup requirements, and whether the work could instead happen during render.
When should I use useMemo or useCallback?
Use useMemo for identified expensive repeated calculations or when a stable value identity is required. Use useCallback when a stable function identity benefits a specific memoized child or hook, and begin performance decisions with profiling.
How should I approach React debugging interview questions?
Reproduce the problem, isolate the fault, inspect the relevant state and identities, verify timing, and apply the smallest correction. Explain your hypothesis before changing the code.
How do I decide where React state should live?
Identify who reads and writes the state, how often it changes, whether it is local, shared, or server-owned, and what happens when an update fails. Keep state local when possible, lift it for sibling coordination, and use context or external stores when their sharing model fits.

Keep reading

Ace your next coding interview

Stealth Interview is a desktop app for macOS and Windows that reads the problem off your screen and answers with a working solution, a step-by-step explanation and its time and space complexity — while staying invisible to screen sharing.

Get Stealth Interview