useState

Master useState — the most fundamental React hook. Learn how it works under the hood, the patterns senior engineers use, and the bugs that trip up even experienced developers.

Last reviewed: January 2025

·React 16.8+React 18React 19
✅
Quick Understanding: useState is a hook that adds state to a function component. It returns a pair: the current value and a function to update it. Calling the updater function tells React to re-render the component with the new value. It is the most-used hook in every React codebase.

🔍 What Is It?

useState is a React hook that lets you add reactive state to a function component.

const [value, setValue] = useState<Type>(initialValue);
  • →value — the current state (read-only inside this render)
  • →setValue — the updater function that schedules a re-render
  • →initialValue — the value on the first render only (ignored on subsequent renders)

Two update forms:

  • →setValue(newValue) — direct update
  • →setValue(prev => prev + 1) — functional update, safe when new value depends on old value

🎯 Why Do We Need It?

Before hooks, only class components could have state. Function components were purely presentational. useState changed that — now any function can hold reactive data.

Without useState, you cannot build:

  • →A form that tracks user input
  • →A button that shows loading state
  • →A modal that opens and closes
  • →A counter, a toggle, a dropdown — any interactive UI element

useState is the foundation of all React interactivity.

🏗️ What Happens in Real Projects?

In an e-commerce checkout flow:

  • →const [step, setStep] = useState<1|2|3>(1) — which checkout step is active
  • →const [cart, setCart] = useState<CartItem[]>([]) — items in the cart
  • →const [coupon, setCoupon] = useState('') — coupon code input
  • →const [processing, setProcessing] = useState(false) — payment in flight
  • →const [error, setError] = useState<string | null>(null) — payment error message

⚙️ How It Works

Under the hood:

React maintains a list of state slots for each component instance. On the first render, useState initialises the slot. On subsequent renders, it returns the current value from that slot. This is why hooks must always be called in the same order — React uses call order to match hooks to their slots.

Batching (React 18+):

Multiple setState calls in the same event handler are batched — React runs only one re-render for all of them. In React 18, this batching also applies inside async functions, timeouts, and promises.

Object state:

useState does a shallow replace, not a deep merge. If your state is an object, you must spread the previous state: setUser(prev => ({ ...prev, name: 'Alice' })). Forgetting the spread overwrites the entire object.

💻 Example

useState Patterns You Will Use Daily — Primitives, objects, arrays, and the async state pattern.

tsx
// Boolean toggle
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(prev => !prev);

// String input
const [search, setSearch] = useState('');

// Enum-like state
type Tab = 'overview' | 'claims' | 'documents';
const [activeTab, setActiveTab] = useState<Tab>('overview');

// Nullable async result
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

🤔 Common Confusions

❓ Why does my state show the old value right after setState?

setState is asynchronous — it schedules a re-render but the current render's variable is already captured. console.log(count) after setCount(5) still shows the old count. The new value is available in the next render. For logic that needs the new value immediately, compute it as a local variable first.

❓ What is the stale closure problem?

A function defined inside a component captures the state value at the time it was created. If state updates but the function is not recreated, it still sees the old value. Fix: use the functional update form setCount(prev => prev + 1) so the update always uses the latest value, regardless of closure.

❓ When should I use multiple useState vs one object?

Use multiple useState for independent values that change separately (isOpen, activeTab, searchTerm). Use one object for related values that often change together (form fields, async state: {data, loading, error}). The rule: if you always update them together, group them.

🚨 Common Production Issues

**Bug: Stale state in async callbacks** — An API callback captures the state from when the effect ran. By the time it resolves, state has changed. Fix: use functional updates or useRef to track the latest value.

**Bug: Object state overwrite** — setUser({ name: "Alice" }) throws away all other user fields. Always spread: setUser(prev => ({ ...prev, name: "Alice" })).

**Performance: Initialising state with expensive computation** — useState(computeHeavyValue()) runs on every render. Fix: pass a function useState(() => computeHeavyValue()) — runs only once.

**Bug: State not updating in loops** — Calling setCount(count + 1) three times in a loop gives count + 1, not count + 3, because count is the same snapshot in all three calls. Fix: setCount(prev => prev + 1).

✅ Best Practices

Use functional updates when new state depends on previous state

Use lazy initialisation for expensive initial values: useState(() => fn())

Group related state into objects; keep independent state separate

Always spread object state when doing partial updates

Use immutable array patterns (map, filter, spread) — never push/splice

Name state variables clearly: isLoading not loading, selectedUserId not user

Co-locate state with the component that uses it — do not hoist unnecessarily

🏛️ Architect's Note

Senior Architect's Perspective

useState is the right tool for local, component-specific state. The mistake teams make is using useState for everything — including server data, global UI state, and complex multi-step workflows. Server data belongs in React Query. Global UI state (sidebar open, theme) belongs in Context or Zustand. Complex workflows belong in useReducer. useState is powerful but has a clear scope: simple, local, component-owned state.

💬 Frequently Asked Questions

Can I call useState conditionally?

No. Hooks must be called unconditionally at the top level of your component. Calling useState inside an if block or loop violates the Rules of Hooks and causes React to misalign its internal hook slots, leading to bugs. ESLint's react-hooks/rules-of-hooks plugin catches this.

Is setState synchronous or asynchronous?

The state update is asynchronous — React batches updates and applies them before the next render. However, React 18 guarantees that all state updates within an event handler are applied in a single re-render. The re-render itself happens synchronously after all updates are batched.

What happens if I call setState with the same value?

React uses Object.is() to compare the new value to the current value. If they are the same, React bails out and does not re-render. This is why mutating state directly (array.push()) does not trigger a re-render — the reference is the same.

🎯 Key Takeaways

1useState returns [currentValue, updaterFunction] — always destructure
2The initial value is only used on the first render
3State updates are asynchronous — the new value is in the next render
4Use functional updates setState(prev => ...) when new state depends on old
5Never mutate state directly — create new objects and arrays
6Use lazy initialisation for expensive computations: useState(() => fn())
7Multiple setState calls in one event handler are batched into one re-render

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these React references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages

SplashRide React learning path

Browse the full React topic collection and learning order on SplashRide.

React Interview Prep

Practice React interview questions and scenario-based answers.

Frontend Engineer Career Path

A beginner-friendly journey through web foundations, React, production frontend engineering, and interview preparation.

Python Developer Career Path

A practical route through Python fundamentals, automation, backend basics, testing, projects, interview preparation, and career growth.

React Controlled Components: Inputs, Checkboxes, and Forms

Build React controlled inputs with value, checked, and onChange. Includes a complete form, reset behavior, number fields, and fixes for uncontrolled-input warnings.

State

Understand React state — the internal memory of a component that drives UI updates. Learn when to use local state, how to structure it, and the patterns senior engineers use to avoid the most common state bugs.