LearnContact
Lesson 2822 min read

useState Hook

Master the useState Hook in depth, including functional updates, immutable updates to objects and arrays, and lazy initial state.

Introduction

useState is the Hook you will reach for most often in React. It gives a function component the ability to remember a value between renders and to trigger a re-render whenever that value changes — the foundation of every interactive UI, from a simple counter to a complex form.

This lesson goes beyond the basics you may have already seen. You will learn the correct way to update state based on its previous value, how to update objects and arrays without mutating them directly, and how to avoid wasting performance on an expensive initial value using lazy initialization.

What You Will Learn
  • The basic useState syntax and how state persists between renders.
  • Why functional updates (prev => prev + 1) matter for correctness.
  • How to update objects immutably using the spread operator.
  • How to update arrays immutably (adding, removing, editing items).
  • How to use lazy initial state for expensive calculations.

useState Basics

Calling useState(initialValue) returns an array with exactly two items: the current value of the state, and a function to update it. By convention, you destructure this array using array destructuring, naming the pair [value, setValue].

Counter.jsx
import { useState } from 'react';

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
Rendered on the Page
Count: 0
[Increment]

(clicking the button re-renders the component with the updated count)

Calling setCount schedules a re-render of the component with the new value. Unlike a regular variable, count survives between renders — React stores it internally and hands it back to your component function each time it runs.

initialValue

The argument passed to useState the very first time — ignored on every subsequent render.

Current Value

The first array item always reflects state as of the render currently executing.

Setter Function

The second array item schedules an update and triggers a re-render with the new value.

Independent State

Each call to useState creates its own independent piece of state, tracked separately.

Functional Updates

State updates in React can be batched and are not always applied immediately. When your new state depends on the previous state, passing a plain value like setCount(count + 1) can read a stale value of count if multiple updates happen close together. The safer approach is a functional update: passing a function to the setter, which React guarantees receives the most up-to-date previous state.

The Problem With Direct Updates
function handleTripleIncrement() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  // count only increases by 1, not 3 —
  // all three calls read the same stale "count" from this render.
}
The Fix: Functional Updates
function handleTripleIncrement() {
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  // count correctly increases by 3 —
  // each call receives the latest pending value as "prev".
}
Rule of Thumb

Whenever your next state depends on the current state (incrementing, toggling, appending), use the functional form setValue(prev => ...) instead of referencing the outer variable directly.

Updating Objects Immutably

React decides whether to re-render by comparing the previous state value to the new one. If you mutate an object in place and pass the same reference back to the setter, React sees no change and will not re-render. Instead, always create a new object — commonly using the spread operator — copying the existing fields and overriding only what changed.

Wrong: Mutating State Directly
function ProfileForm() {
  const [user, setUser] = useState({ name: 'Amol', age: 25 });

  function updateAge() {
    user.age = 26;       // ❌ mutates the existing object
    setUser(user);        // React sees the same reference — no re-render
  }
}
Correct: Spreading Into a New Object
function ProfileForm() {
  const [user, setUser] = useState({ name: 'Amol', age: 25 });

  function updateAge() {
    setUser(prev => ({ ...prev, age: 26 })); // ✅ new object, same other fields
  }

  return <p>{user.name} is {user.age} years old</p>;
}
Rendered on the Page
Amol is 26 years old

Updating Arrays Immutably

The same immutability rule applies to arrays stored in state. Methods like push, splice, and sort mutate the array in place and should be avoided on state. Instead, use methods that return a new array, such as spreading, map, and filter.

TodoList.jsx
function TodoList() {
  const [todos, setTodos] = useState(['Learn JSX', 'Learn Hooks']);

  function addTodo(text) {
    setTodos(prev => [...prev, text]);              // add: spread + new item
  }

  function removeTodo(index) {
    setTodos(prev => prev.filter((_, i) => i !== index)); // remove: filter
  }

  function editTodo(index, newText) {
    setTodos(prev => prev.map((t, i) => (i === index ? newText : t))); // edit: map
  }

  return (
    <ul>
      {todos.map((todo, i) => <li key={i}>{todo}</li>)}
    </ul>
  );
}
OperationMutating (Avoid)Immutable (Use This)
Add itemtodos.push(text)[...todos, text]
Remove itemtodos.splice(i, 1)todos.filter((_, idx) => idx !== i)
Edit itemtodos[i] = newTexttodos.map((t, idx) => idx === i ? newText : t)
Sort itemstodos.sort()[...todos].sort()

Lazy Initial State

Normally, whatever value you pass to useState is only used on the very first render — but if that value comes from an expensive calculation, that calculation still runs on every single render, only to be thrown away after the first time. React lets you avoid this by passing a function to useState instead of a value; React will call that function only once, on mount, to compute the initial state.

Wasteful: Recalculated Every Render
function ProductList() {
  // expensiveFilter() runs on EVERY render, even though
  // its result is discarded after the first one.
  const [items, setItems] = useState(expensiveFilter(allProducts));
}
Efficient: Lazy Initial State
function ProductList() {
  // expensiveFilter() runs only ONCE, on the initial render.
  const [items, setItems] = useState(() => expensiveFilter(allProducts));
}
When to Use This

Reach for lazy initial state when computing the initial value involves real work — reading and parsing localStorage, filtering or sorting a large array, or any non-trivial computation. For simple literals like useState(0) or useState(""), it makes no difference.

Common Mistakes

Avoid These Mistakes
  • Mutating an object or array in state directly (obj.field = x, arr.push(x)) instead of creating a new copy.
  • Using setCount(count + 1) multiple times in a row when you meant to accumulate — use the functional form instead.
  • Calling an expensive function directly inside useState(expensiveFn()) instead of passing it as useState(() => expensiveFn()).
  • Forgetting that calling the setter with the same value/reference will not trigger a re-render, since React skips updates when nothing actually changed.

Best Practices

  • Use functional updates (prev => ...) whenever the new state depends on the previous state.
  • Always spread objects and arrays to create new copies rather than mutating state directly.
  • Pass a function to useState (lazy initialization) for any initial value that requires real computation.
  • Keep each useState call focused on one logical piece of state rather than one giant state object for everything.
  • Name your state and setter pairs clearly and consistently, e.g. const [isOpen, setIsOpen] = useState(false).

Frequently Asked Questions

Why doesn't my component re-render after I update an array in state?

You likely mutated the array in place (e.g. with push or splice) and passed the same reference back to the setter. React compares references, so it sees no change. Create a new array instead, e.g. with spread or filter.

Is setCount asynchronous?

State updates are not applied instantly — React can batch multiple updates together and process them before the next render, which is why relying on the outer variable right after calling the setter can show a stale value.

When should I use a single state object instead of multiple useState calls?

Group values into one object only when they are logically related and usually updated together, like form fields. Otherwise, prefer separate useState calls for independent values — it keeps updates simpler.

Does calling the setter with the exact same value still re-render?

No. React uses a quick reference/value comparison (Object.is) and will skip re-rendering if the new state is identical to the current state.

What is the difference between useState(fn()) and useState(() => fn())?

useState(fn()) calls fn() on every render, discarding the result after the first. useState(() => fn()) passes the function itself, so React only invokes it once, on the initial render.

Key Takeaways

  • useState(initialValue) returns [value, setValue] and persists state between renders.
  • Use functional updates, setValue(prev => ...), whenever new state depends on old state.
  • Always update objects and arrays immutably by creating new copies, never mutating in place.
  • Pass a function to useState for lazy initialization when the initial value is expensive to compute.
  • React skips a re-render if the new state is reference/value-equal to the current state.

Summary

useState is deceptively simple on the surface, but writing correct, bug-free state updates depends on a few important habits: preferring functional updates when new state depends on old state, always copying objects and arrays instead of mutating them, and using lazy initialization for anything expensive to compute.

With state under control, the next step is learning how to run code in response to state changes and component lifecycle events — which is exactly what the useEffect Hook is for.

Lesson 28 Completed
  • You can use useState to add state to a function component.
  • You understand why and when to use functional updates.
  • You can update objects and arrays immutably.
  • You know how and when to use lazy initial state.
Next Lesson →

useEffect Hook