Why useState() exists in React (and what it actually guarantees)

MediumIntermediateReact
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

Explain what useState() is for (local component state), what it returns, how updates trigger re-renders, and the key mental models interviewers expect: initial state (including lazy init), functional updates, batching, Object.is bailout, and why you must treat state as immutable.

Answer

Core idea

useState() gives a function component persistent local state across renders. When you call its setter, React schedules a re-render of that component so the UI can be recalculated from the new state. The key model is: UI = f(props, state) — you don’t manually update the DOM.

What you get

What it means

Why it matters

A state value

The current snapshot for this render

Rendering reads state; it does not mutate it

A setter function

Schedules an update (it does not instantly change the variable)

React decides when to render/commit; avoids inconsistent UI

State is tied to position in the tree

State is preserved across renders while the component stays mounted

Unmount/remount (or key changes) resets state

What useState actually provides
JSX
import React from 'react';

export default function Counter() {
  const [count, setCount] = React.useState(0);

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Count: {count}
    </button>
  );
}
                  

Why the functional update form matters

If the next state depends on the previous state, use setX(prev => next). This avoids stale reads when multiple updates happen close together or are batched.

JSX
function Bad() {
  const [n, setN] = React.useState(0);

  // ❌ can be stale if called multiple times in one tick
  const incTwice = () => {
    setN(n + 1);
    setN(n + 1);
  };

  return <button onClick={incTwice}>{n}</button>;
}

function Good() {
  const [n, setN] = React.useState(0);

  // ✅ each update reads the latest queued value
  const incTwice = () => {
    setN((x) => x + 1);
    setN((x) => x + 1);
  };

  return <button onClick={incTwice}>{n}</button>;
}
                  

Interview-level detail

Correct statement

Practical takeaway

Initial state

useState(initial) uses the initial value only on the first mount

If you need expensive computation, use lazy init: useState(() => compute())

Bailout

If the new state is Object.is-equal to the old state, React can skip re-rendering

Don’t create new objects/arrays unless something actually changed

Batching

React may batch multiple state updates into one render

Don’t rely on “immediate” state after calling the setter

Immutability

Mutating state in place breaks React’s change detection assumptions

Always create new references when updating objects/arrays

The key guarantees and caveats around useState
JSX
function TodoApp() {
  // ✅ Lazy init (runs only once on mount)
  const [todos, setTodos] = React.useState(() => {
    const raw = localStorage.getItem('todos');
    return raw ? JSON.parse(raw) : [];
  });

  // ✅ Immutable update (new array)
  function addTodo(text) {
    setTodos((prev) => [...prev, { id: crypto.randomUUID(), text }]);
  }

  // ❌ Bad: mutating state
  function badAdd(text) {
    todos.push({ id: 'x', text });
    setTodos(todos); // same reference -> may not re-render
  }

  return (
    <div>
      <button onClick={() => addTodo('Learn useState')}>Add</button>
      <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
    </div>
  );
}
                  

When useState is the wrong tool

useState is for local UI state (toggles, inputs, selected tabs). If updates become complex (many transitions, derived actions), useReducer often reads better. If the state is global/shared across many branches, you typically lift it up, use Context carefully, or use a store.

Summary

useState() adds persistent local state to function components. It returns [state, setState]; calling the setter schedules a re-render so React can recompute the UI. Use functional updates when the next state depends on the previous state, update objects/arrays immutably, and remember that React may batch updates and can bail out when the new state is Object.is-equal to the old state.

Similar questions
Guides
21 / 41