React useState

useState gives a function component its own piece of memory that survives re-renders and triggers a new render whenever it changes.

Declaring State

Calling useState(initialValue) returns a two-element array: the current value and a setter function to update it. Array destructuring — const [value, setValue] = useState(initial) — lets you name both however you like. The argument you pass is used only on the very first render; every render after that ignores it.

Updating State Correctly

Unlike the setState method on class components, the setter returned by useState replaces the value entirely — it does not merge objects for you. When the next value depends on the current one, pass the setter a function instead of a value; React calls it with the latest state and uses its return value, which avoids bugs caused by stale values inside closures.

  • The initial value argument is only used on the component's first render
  • Calling the setter schedules a re-render; it does not update the variable immediately in the same line
  • State is preserved between renders as long as the component stays mounted in the same position in the tree
  • Never mutate state directly (no array.push or object.field = x); always create a new value
  • Use the functional updater form — setCount(prev => prev + 1) — whenever the new value depends on the old one

A counter with functional updates

import { useState } from "react";

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

  function increment() {
    setCount((prev) => prev + 1);
  }

  function decrement() {
    setCount((prev) => prev - 1);
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  );
}

export default Counter;

State does not have to be a single primitive value — it can just as easily hold an object or an array, as long as you replace it immutably. Spreading the previous value into a new object or array keeps the fields you did not touch while updating the ones you did.

Object state in a form

import { useState } from "react";

function SignupForm() {
  const [form, setForm] = useState({ name: "", email: "" });

  function handleChange(event) {
    const { name, value } = event.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  }

  return (
    <form>
      <input name="name" value={form.name} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      <p>Hello, {form.name || "stranger"}!</p>
    </form>
  );
}

export default SignupForm;
Note: Pass a function to useState — lazy initialization — when the initial value is expensive to compute, such as useState(() => expensiveCalc()). React calls that function only once, on the first render, instead of re-running it on every render.

Array state for a todo list

import { useState } from "react";

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

  function addTodo() {
    if (text.trim() === "") return;
    setTodos((prev) => [...prev, text]);
    setText("");
  }

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

export default TodoList;
PatternExampleNotes
PrimitiveuseState(0)Replace directly: setCount(5) or setCount(c => c + 1)
ObjectuseState({ name: "" })Spread the previous object so untouched fields survive: setForm(p => ({...p, name}))
ArrayuseState([])Create a new array with spread, map, or filter — never push or splice the existing one
Lazy inituseState(() => expensiveCalc())Pass a function so the expensive calculation runs only once, on mount
Note: Because setState calls inside the same event handler are batched, reading count right after calling setCount(count + 1) still shows the old value on that line — use the functional updater form whenever the next state depends on the previous one.

Exercise: React useState

Right after calling a state setter, what does the local variable holding the old state show?