Skip to the solution
EasyFrontend6 min read

How do you manage component-level state without useState or useReducer?

280 views

The problem

Question presented to candidate: "Suppose you cannot use useState or useReducer. What other ways are there to hold state that belongs to a component?"

What a strong answer should cover:

  • The historical answer: class components with this.state and this.setState. Still fully supported in React 19 β€” verified working.
  • this.setState merges partial state, unlike the hook setters which replace; and it takes an optional callback that runs after the commit.
  • useRef for values that must persist across renders but must not trigger one β€” timer ids, previous values, DOM nodes, instance-like flags.
  • The key distinction: state drives rendering; a ref does not. If the UI must react to the value, it is state.
  • useSyncExternalStore for reading state that lives outside React entirely.
  • useState and useReducer are two faces of one primitive, so "without either" really means "outside the hooks state model".
  • Anti-patterns: a module-level variable (shared by every instance and invisible to React), or mutating a ref and expecting a re-render.
  • Practical framing: this question is usually probing whether you understand when a value should cause a render, not whether you can recite class syntax.

Clarifying questions expected:

  • "Does the UI need to update when this value changes?" β€” that single question decides state versus ref.
  • "Is this a legacy codebase, or are we designing something new?"

Code / implementation expected: Yes β€” a class component with setState beside a useRef example showing why the ref does not re-render.

  • state management
  • class components
  • legacy
  • setstate

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactClass setState, a ref that does not render, and the module-variable trap
import { Component, useState, useRef } from "react";

// ── 1. The classic answer: a class component ──────────────────────────────
class ClassCounter extends Component {
  state = { count: 0, label: "class state" };

  bump = () => {
    // Note: setState MERGES. label survives without being mentioned.
    this.setState(
      (s) => ({ count: s.count + 1 }),
      // A post-commit callback β€” no hook equivalent; use an effect instead.
      () => console.log("committed, count is now", this.state.count),
    );
  };

  render() {
    return (
      <Row title="class this.setState">
        {this.state.label}: <strong>{this.state.count}</strong>{" "}
        <button onClick={this.bump}>+1</button>
      </Row>
    );
  }
}

// ── 2. useRef: persists, but deliberately never re-renders ────────────────
function RefCounter() {
  const clicks = useRef(0);
  const renders = useRef(0);
  const [, forceRender] = useState(0);
  renders.current++;

  return (
    <Row title="useRef">
      ref value: <strong>{clicks.current}</strong> Β· renders:{" "}
      <strong>{renders.current}</strong>{" "}
      <button onClick={() => { clicks.current++; }}>
        mutate ref (no re-render)
      </button>{" "}
      <button onClick={() => forceRender((n) => n + 1)}>force a render</button>
    </Row>
  );
}

// ── 3. The trap: a module-level variable ──────────────────────────────────
// Shared by EVERY instance, and React has no idea it changed.
let shared = 0;
function ModuleVarCounter({ name }) {
  const [, forceRender] = useState(0);
  return (
    <Row title={"module variable (" + name + ")"}>
      value: <strong>{shared}</strong>{" "}
      <button onClick={() => { shared++; }}>mutate (UI will not update)</button>{" "}
      <button onClick={() => forceRender((n) => n + 1)}>force a render</button>
    </Row>
  );
}

function Row({ title, children }) {
  return (
    <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, marginBottom: 10 }}>
      <div style={{ fontSize: 12, color: "#666", marginBottom: 4 }}>{title}</div>
      {children}
    </div>
  );
}

export default function App() {
  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <ClassCounter />
      <RefCounter />
      <ModuleVarCounter name="instance one" />
      <ModuleVarCounter name="instance two" />
      <p style={{ color: "#666", fontSize: 13 }}>
        Mutate the ref a few times, then force a render β€” the number jumps to
        where it already was. Do the same on either module-variable row and
        both rows jump together, because they share one value.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 21 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextEasy

What is the difference between state and props?

Continue