useState Hook đŸĒ

1. Introduction 👋

useState is the most fundamental Hook in React — it's usually the very first Hook developers learn. This tutorial takes a deep, focused look at exactly how useState works internally, covering initialization, updates, batching, and the patterns that separate clean state management from subtle, hard-to-debug issues.

Information

This tutorial assumes familiarity with Components and JSX. If you want a broader overview of state concepts first, see the State tutorial.

2. What is useState? 🤔

useState is a Hook that adds a piece of local, reactive state to a function component. It returns a pair: the current value and a function to update it.

Code Snippet

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}
>>"useState is a React Hook that lets you add a state variable to your component." — React Documentation

3. Why Use useState? 💡

  • Reactivity: Updating state automatically triggers a re-render with the new value.
  • Persistence Across Renders: Unlike a regular variable, state survives between renders.
  • Encapsulation: Each component instance maintains its own independent state.
  • Simplicity: A minimal API for the most common state management needs.

4. Creating State 🌱

Code Snippet

const [value, setValue] = useState(initialValue);

Calling useState declares a new independent state variable. The array destructuring syntax gives you full control over naming both the value and its setter.

Code Snippet

function ProfileForm() {
  const [name, setName] = useState("");
  const [age, setAge] = useState(18);
  const [isSubscribed, setIsSubscribed] = useState(false);
}

5. Reading State 👀

The current state value behaves like a normal constant during a given render — it never changes mid-render, only between renders.

Code Snippet

function Greeting() {
  const [name] = useState("Alice");
  return <h1>Hello, {name}!</h1>;
}

Note

Since value is effectively a const within a render, you'll get a linting error if you try to reassign it directly — always use the setter instead.

6. Updating State âœī¸

Calling the setter function schedules a state update and triggers a re-render for that component (and, if needed, its children).

Code Snippet

function Toggle() {
  const [isOn, setIsOn] = useState(false);

  return (
    <button onClick={() => setIsOn(!isOn)}>
      {isOn ? "ON" : "OFF"}
    </button>
  );
}

Important

Calling the setter with the same value as the current state (checked via Object.is) skips the re-render entirely as an optimization.

7. Initial State 🌟

The value passed into useState is used only on the first render. On every subsequent render, React ignores this argument entirely and returns the current stored state.

Code Snippet

function Timer() {
  // "0" is only used once, on mount
  const [seconds, setSeconds] = useState(0);
}

8. Lazy Initialization 💤

When computing the initial value is expensive, pass a function instead of a direct value — React calls it exactly once, on the first render only.

Code Snippet

function TodoApp() {
  // ✅ Function only runs once
  const [todos, setTodos] = useState(() => loadTodosFromStorage());

  // ❌ Runs on every render, even though the result is discarded after mount
  // const [todos, setTodos] = useState(loadTodosFromStorage());
}

Best Practice

Use lazy initialization for anything involving localStorage reads, JSON parsing, or other non-trivial computation.

9. Multiple State Variables đŸ”ĸ

It's idiomatic in React to declare several independent useState calls rather than combining unrelated values into a single state object.

Code Snippet

function SignupForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [errors, setErrors] = useState({});
}

Tip

Split state by variable when values update independently; group into one object only when values are always updated together.

10. Functional Updates 🔁

When the new state depends on the previous value, pass a function to the setter instead of a direct value — this guarantees correctness even with rapid or batched updates.

Code Snippet

function Counter() {
  const [count, setCount] = useState(0);

  function incrementThreeTimes() {
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
    // Correctly results in +3
  }
}

Warning

Calling setCount(count + 1) three times in a row only adds 1 total, since each call captures the same stale count from the current render's closure.

11. Updating Objects 🧱

Since state must remain immutable, updating an object means creating a brand-new object rather than modifying the existing one in place.

Code Snippet

function ProfileForm() {
  const [user, setUser] = useState({ name: "Alice", age: 25 });

  function updateName(newName) {
    setUser({ ...user, name: newName });
  }
}

Danger

Writing user.name = newName directly mutates the existing object without changing its reference, so React won't detect the change and won't re-render.

12. Updating Arrays 📚

Code Snippet

function TodoList() {
  const [todos, setTodos] = useState([]);

  function addTodo(text) {
    setTodos([...todos, { id: crypto.randomUUID(), text }]); // ✅ new array
  }

  function toggleTodo(id) {
    setTodos(todos.map((todo) =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    )); // ✅ new array, new object for the changed item
  }
}

Caution

Avoid mutating methods like push(), splice(), or sort() directly on state arrays — always produce a new array instead.

13. Nested State đŸĒ†

Updating deeply nested state requires copying every level of the structure that changes, since immutability must be preserved at each nested layer.

Code Snippet

function Settings() {
  const [preferences, setPreferences] = useState({
    theme: "light",
    notifications: { email: true, sms: false },
  });

  function toggleSms() {
    setPreferences({
      ...preferences,
      notifications: {
        ...preferences.notifications,
        sms: !preferences.notifications.sms,
      },
    });
  }
}

Tip

For deeply nested structures, consider flattening your state shape, or using a helper library like immer to simplify updates.

14. State Immutability 🔒

React determines whether to re-render by comparing the previous and next state references. Mutating state in place keeps the reference identical, so React has no way of detecting the change.

Mutating (Avoid)Immutable (Prefer)
obj.key = value{ ...obj, key: value }
array.push(item)[...array, item]
array[i] = newValarray.map((v, idx) => idx === i ? newVal : v)

15. State Batching đŸŽ¯

React batches multiple state updates that happen within the same event handler into a single re-render, avoiding redundant work.

Code Snippet

function Form() {
  const [name, setName] = useState("");
  const [submitted, setSubmitted] = useState(false);

  function handleSubmit() {
    setName("Alice");
    setSubmitted(true);
    // Only ONE re-render occurs, not two
  }
}

Information

Since React 18, automatic batching applies everywhere — including inside promises, timeouts, and native event handlers, not just React's own synthetic events.

16. State Queue đŸ“Ĩ

When several updates are triggered before the next render, React processes them in order as a queue. Functional updates ensure each step in the queue sees the correctly updated value from the previous step.

Code Snippet

function QueueDemo() {
  const [number, setNumber] = useState(0);

  function handleClick() {
    setNumber((n) => n + 1); // 0 -> 1
    setNumber((n) => n + 1); // 1 -> 2
    setNumber((n) => n * 2); // 2 -> 4
  }

  return <button onClick={handleClick}>{number}</button>; // results in 4
}

17. Asynchronous State Updates âąī¸

State updates are not applied immediately — the current render's variable keeps its original value until the component re-renders with the new state.

Code Snippet

function Example() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
    console.log(count); // logs the OLD value, not the updated one
  }
}

Caution

To react to a state change after it's applied, use a useEffect that depends on that state, rather than reading it right after calling the setter.

18. Derived State 🧮

Not every piece of UI data needs its own useState. If a value can be calculated from existing state or props, compute it directly during render instead of storing a redundant copy.

Code Snippet

function Cart({ items }) {
  // ✅ Derived directly — always in sync with 'items'
  const total = items.reduce((sum, item) => sum + item.price, 0);

  return <p>Total: ${total.toFixed(2)}</p>;
}

Best Practice

Storing a derived value in its own useState risks it going out of sync with the source data — prefer computing it fresh on every render.

19. Resetting State 🔄

Changing a component's key prop forces React to treat it as an entirely new instance, discarding all of its previous state and starting fresh.

Code Snippet

function ProfileEditor({ userId }) {
  return <Form key={userId} />;
  // Changing userId fully resets Form's internal useState values
}

Tip

This is a common pattern for resetting form fields when switching between different records, like editing different users one after another.

20. Preserving State 💾

React preserves a component's state across re-renders as long as it stays at the same position in the tree with the same type and key.

Code Snippet

function App({ showForm }) {
  return (
    <div>
      {showForm && <SignupForm />}
      {/* SignupForm's state resets each time it's unmounted and remounted */}
    </div>
  );
}

Note

Conditionally unmounting a component (removing it from the tree entirely) resets its state — this differs from just hiding it visually with CSS.

21. Sharing State 🔗

A single useState call is local to one component instance. To share state between siblings, it must be lifted to their closest common parent, or managed via Context.

Code Snippet

function Parent() {
  const [selectedTab, setSelectedTab] = useState("home");

  return (
    <>
      <TabBar selected={selectedTab} onSelect={setSelectedTab} />
      <TabContent tab={selectedTab} />
    </>
  );
}

22. Lifting State Up âŦ†ī¸

Lifting state up means moving a useState call from a child component to a shared parent, then passing the value and setter down as props.

Code Snippet

// Before: state trapped in a single child, siblings can't access it
function Child() {
  const [value, setValue] = useState("");
}

// After: lifted to the parent, shared between siblings
function Parent() {
  const [value, setValue] = useState("");
  return (
    <>
      <Input value={value} onChange={setValue} />
      <Preview value={value} />
    </>
  );
}

23. State Performance ⚡

  • Placing state too high in the tree can cause unnecessary re-renders of unrelated child components.
  • Split unrelated pieces of state into separate useState calls to limit re-render scope.
  • Use useMemo for expensive derived computations based on state.
  • Avoid storing large, frequently changing objects in a single useState if only a small part actually changes often.

Tip

Colocate state as close as possible to where it's actually used — this naturally limits the scope of re-renders triggered by updates.

24. Common State Patterns 🎨

Code Snippet

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

Code Snippet

const [count, setCount] = useState(0);
const increment = () => setCount((c) => c + 1);
const decrement = () => setCount((c) => c - 1);
const reset = () => setCount(0);

Code Snippet

const [formData, setFormData] = useState({ email: "", password: "" });

function handleChange(e) {
  setFormData({ ...formData, [e.target.name]: e.target.value });
}

25. TypeScript with useState 🔷

Code Snippet

// Inferred as boolean
const [isVisible, setIsVisible] = useState(false);

// Explicit generic for complex or nullable state
interface User {
  id: string;
  name: string;
}

const [user, setUser] = useState<User | null>(null);

// Explicit generic for a union of specific string values
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");

Tip

Use an explicit generic (useState<T>(...)) whenever the initial value alone isn't enough for TS to infer the full intended type, such as null placeholders.

26. Best Practices 🌟

  1. Keep each useState call focused on a single, independent piece of data.
  2. Use functional updates whenever new state depends on the previous state.
  3. Always treat state as immutable — create new objects and arrays rather than mutating.
  4. Use lazy initialization for expensive initial computations.
  5. Avoid storing values that can be derived from existing state or props.
  6. Colocate state close to where it's used to minimize unnecessary re-renders.

27. Common Mistakes đŸšĢ

  • Directly mutating state objects or arrays instead of creating new copies.
  • Assuming state updates immediately, and reading the old value right after calling the setter.
  • Calling the setter multiple times with a direct value instead of a functional update when relying on the previous state.
  • Storing derived values in state instead of computing them during render.
  • Passing an expensive function call directly to useState() instead of wrapping it for lazy initialization.

Danger

Directly mutating an array (e.g., todos.push(newTodo)) followed by setTodos(todos) passes the same reference back to React, which may skip the re-render entirely.

28. Frequently Asked Questions ❓

Question

Why doesn't my state update right after calling the setter?

Answer

State updates are asynchronous and scheduled — the variable in the current render retains its old value until the component re-renders with the new state.

Question

Should I use one useState object or several separate calls?

Answer

Prefer separate calls for values that update independently; group values into one object only when they're always updated together.

Question

Is useState enough for complex, interrelated state logic?

Answer

For simple to moderate cases, yes. For complex state transitions involving multiple related values, useReducer is often a clearer alternative.

29. Summary 📝

useState is the foundation of interactivity in React function components, giving them the ability to remember and update data over time. Mastering its nuances — immutability, functional updates, batching, and lazy initialization — is essential for writing correct, performant React applications.

Summary

With useState covered in depth, the natural next step is exploring useReducer for more complex state logic, and useEffect for synchronizing state with external systems.