Beginner30 min read

State with useState

Learn how to make your React components interactive by managing dynamic data with the useState hook.

Why Do We Need State?

So far, our components have only displayed static data passed through props. But real applications need to respond to user actions — incrementing a counter, toggling a menu, filtering a list, typing in a search box. For this, we need state.

State is data that belongs to a component and can change over time. When state changes, React automatically re-renders the component to reflect the new data. This is the core of React's reactivity.

Consider this broken attempt at making a counter:

jsx
function Counter() {
  let count = 0;
  
  function handleClick() {
    count = count + 1;
    console.log(count); // This increments, but...
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

Clicking the button changes the count variable, but the screen never updates. Why? Because React does not know the variable changed — it has no reason to re-render the component.

To solve this, React provides the useState hook. Hooks are special functions that let you "hook into" React features from function components. useState is the most fundamental hook — it gives your component a piece of state that React tracks and re-renders when it changes.

The difference between props and state:

PropsState
Passed from parentManaged inside the component
Read-only (immutable)Can be updated by the component
The parent controls themThe component controls it
Changing props re-renders the childChanging state re-renders the component

Using the useState Hook

The useState hook is called inside your component function. It takes an initial value as an argument and returns an array with exactly two elements:

  1. The current state value
  2. A function to update the state
jsx
const [count, setCount] = React.useState(0);

This uses array destructuring to name the two values. The convention is [something, setSomething].

Here is the working counter:

jsx
function Counter() {
  const [count, setCount] = React.useState(0);
  
  function handleClick() {
    setCount(count + 1);
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

When you call setCount(count + 1), React:

  1. Updates the state value internally.
  2. Re-renders the component — calls your function again.
  3. This time, useState(0) returns the new value instead of 0.
  4. The JSX now reflects the updated count.

The initial value (0 in this case) is only used on the first render. On subsequent renders, useState returns the current state value.

Important rules for hooks:

  • Only call hooks at the top level of your component — never inside loops, conditions, or nested functions.
  • Only call hooks from React function components or other custom hooks.
jsx
// WRONG — hook inside a condition
function Bad() {
  if (someCondition) {
    const [value, setValue] = React.useState(0); // Never do this!
  }
}

// CORRECT — always at the top level
function Good() {
  const [value, setValue] = React.useState(0);
  // Use conditions in the JSX or handler logic instead
}

State is Immutable: Replace, Don't Mutate

A critical concept in React: you must never mutate state directly. Always use the setter function to create a new value. React relies on detecting that the reference has changed to know when to re-render.

With primitive values (numbers, strings, booleans), this is natural:

jsx
const [count, setCount] = React.useState(0);
setCount(count + 1); // Creates a new number

const [name, setName] = React.useState('');
setName('Alice'); // Creates a new string

With objects and arrays, you must create a new copy instead of modifying the existing one:

jsx
// WRONG — mutating the existing object
const [user, setUser] = React.useState({ name: 'Alice', age: 25 });
user.age = 26; // Mutating the object!
setUser(user); // React sees the same reference — no re-render!

// CORRECT — creating a new object with the spread operator
setUser({ ...user, age: 26 }); // New object, React re-renders

The same applies to arrays:

jsx
const [items, setItems] = React.useState(['A', 'B']);

// WRONG
items.push('C');
setItems(items); // Same reference!

// CORRECT
setItems([...items, 'C']); // New array

Functional updates: When the new state depends on the previous state, use the callback form of the setter to avoid stale values:

jsx
// Basic form — fine for simple cases
setCount(count + 1);

// Functional update — safer, especially in async situations
setCount(prevCount => prevCount + 1);

The functional form receives the most up-to-date previous state value, which prevents bugs when multiple updates happen in the same render cycle.

Multiple State Variables

A component can have as many useState calls as it needs. Each one manages an independent piece of state:

jsx
function UserForm() {
  const [name, setName] = React.useState('');
  const [email, setEmail] = React.useState('');
  const [age, setAge] = React.useState(0);
  
  return (
    <form>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input 
        type="number" 
        value={age} 
        onChange={e => setAge(Number(e.target.value))} 
      />
    </form>
  );
}

Each state variable is completely independent — updating name does not affect email or age.

When to use one state vs. multiple:

  • Use separate state variables when the values change independently. A name field and an age field are updated separately.
  • Use a single state object when the values are closely related and always change together:
jsx
const [position, setPosition] = React.useState({ x: 0, y: 0 });

// Update both at once:
setPosition({ x: event.clientX, y: event.clientY });

State vs. regular variables:

Not all data needs to be state. Use state only for data that:

  1. Changes over time — If a value never changes, use a regular const.
  2. Affects the rendered output — If changing the value should update the UI, it should be state.
  3. Cannot be computed from other state or props — If you can calculate a value from existing state, use a regular variable instead of creating redundant state.
jsx
function Cart({ items }) {
  // Derived value — NOT state, computed from props
  const total = items.reduce((sum, item) => sum + item.price, 0);
  
  // This IS state — changes in response to user action
  const [couponCode, setCouponCode] = React.useState('');
  
  return <p>Total: ${total}</p>;
}

useState in Action

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  function Counter() {
    const [count, setCount] = React.useState(0);

    return (
      <div style={{ textAlign: "center", padding: "20px" }}>
        <h2>Counter: {count}</h2>
        <button onClick={() => setCount(count - 1)}>- Decrease</button>
        {" "}
        <button onClick={() => setCount(0)}>Reset</button>
        {" "}
        <button onClick={() => setCount(count + 1)}>+ Increase</button>
      </div>
    );
  }

  function App() {
    return <Counter />;
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

Why does directly mutating a state variable (like `count = count + 1`) not update the UI?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.