Skip to content
React rc state-events 4 min read

State with useState

Props let data flow into a component, but components also need memory of their own—a value that changes over time and causes the UI to update when it does. That memory is state, and in modern React you manage it with the useState Hook. State is what turns a static render into an interactive interface.

State vs props

Both props and state are plain JavaScript values that influence what a component renders, but they differ in who owns them and whether they can change.

  • Props are passed in by a parent. The receiving component treats them as read-only inputs—it must never reassign them.
  • State is owned and managed inside the component. It is private, mutable through its setter, and changing it tells React to re-render.
PropsState
Owned byParent componentThe component itself
Mutable?No (read-only)Yes (via setter)
Triggers re-render?When parent re-rendersWhen the setter is called
Typical useConfiguration, data passed downValues that change over time

A useful rule of thumb: if a value never changes, make it a constant or a prop. If it changes in response to interaction or time and the component owns that change, it’s state.

Declaring state with useState

useState is a Hook—a special function that lets a component “hook into” React features. Call it at the top level of your component, passing the initial state as its only argument. It returns an array of exactly two items: the current value and a setter function. Destructure them with array syntax.

import { useState } from "react";

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

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

The convention is [thing, setThing]. Here count holds the current value (starting at 0), and setCount is the only sanctioned way to change it.

Always update state through its setter—never mutate the variable directly (count = 5 does nothing useful). React only knows it needs to re-render when you call the setter.

Reading state and the setter

Reading state is just reading the variable: count is a normal number you can use anywhere in your JSX or logic. Updating it means calling the setter with the next value:

setCount(10);     // set to a specific value
setCount(count + 1); // set based on the value this render saw

When you call the setter, React schedules a re-render. On the next render, useState hands back the updated value, so the UI reflects the change automatically. You don’t manually touch the DOM—React reconciles it for you.

Setting state triggers a re-render

This is the heart of React’s reactivity model. Calling a setter does two things: it stores the new value, and it tells React to call your component function again. The returned JSX is compared against the previous output, and only the changed DOM is patched.

function Toggle() {
  const [on, setOn] = useState(false);

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

Output:

First render:  <button>OFF</button>
After click:   <button>ON</button>

Without state, that label would never change. State is the mechanism that connects an event to a visible update.

Initial state and lazy initializers

The argument to useState is used only on the first render; React ignores it on every render afterward. For cheap initial values, pass them directly:

const [name, setName] = useState("");
const [items, setItems] = useState([]);
const [user, setUser] = useState(null);

If computing the initial value is expensive—reading from localStorage, parsing JSON, or running a loop—pass a function instead. React calls it once, on mount, and skips it on later renders. This is a lazy initializer.

function TodoList() {
  // ❌ runs on every render, even though only the first result is used
  const [todos, setTodos] = useState(loadFromStorage());

  // ✅ runs only once, on mount
  const [items, setItems] = useState(() => loadFromStorage());

  return <ul>{items.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}

function loadFromStorage() {
  return JSON.parse(localStorage.getItem("todos") ?? "[]");
}

Note the difference: useState(loadFromStorage()) calls the function on every render and throws away all but the first result, while useState(() => loadFromStorage()) passes a function React invokes a single time.

Multiple state variables

A component can call useState as many times as it needs. Each call is independent and tracked by call order, so keep them at the top level—never inside conditions or loops.

function SignupForm() {
  const [email, setEmail] = useState("");
  const [age, setAge] = useState(18);
  const [agreed, setAgreed] = useState(false);

  const canSubmit = email.includes("@") && agreed;

  return (
    <form>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input
        type="number"
        value={age}
        onChange={(e) => setAge(Number(e.target.value))}
      />
      <label>
        <input
          type="checkbox"
          checked={agreed}
          onChange={(e) => setAgreed(e.target.checked)}
        />
        I agree
      </label>
      <button disabled={!canSubmit}>Sign up</button>
    </form>
  );
}

Splitting unrelated values into separate state variables keeps updates simple and avoids accidentally clobbering one field while changing another. Group values into one state object only when they genuinely change together.

Best Practices

  • Use the setter for every change—treat the state variable as read-only between renders.
  • Pick the smallest possible state. Anything you can compute from existing state or props is derived state, not state.
  • Prefer multiple independent state variables over one large object unless the fields truly move together.
  • Use a lazy initializer (useState(() => …)) whenever the initial value is expensive to produce.
  • Keep useState calls unconditional and at the top level so React’s call-order tracking stays stable.
  • Name your pairs consistently as [value, setValue] for instant readability.
Last updated June 14, 2026
Was this helpful?