Lesson 3 of 12

State & useState

You will learn how to give a component its own changing data with `useState`. When state changes, React updates the screen to match.

JSX
import { useState } from 'react'

function Counter() {
  // useState returns [currentValue, setterFunction]
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  )
}

// When state depends on previous value, use the updater form
function SafeCounter() {
  const [count, setCount] = useState(0)

  const increment = () => setCount(prev => prev + 1)
  // Always safe, even with multiple rapid updates
  return <button onClick={increment}>{count}</button>
}
JSX
import { useState } from 'react'

// State can be any value: string, number, object, array
function ProfileEditor() {
  const [user, setUser] = useState({
    name: "Alice",
    email: "alice@example.com",
    bio: "",
  })

  // Always create new objects/arrays — never mutate state directly
  const updateField = (field, value) => {
    setUser(prev => ({ ...prev, [field]: value }))
  }

  return (
    <form>
      <input
        value={user.name}
        onChange={e => updateField("name", e.target.value)}
        placeholder="Name"
      />
      <input
        value={user.email}
        onChange={e => updateField("email", e.target.value)}
        placeholder="Email"
      />
      <textarea
        value={user.bio}
        onChange={e => updateField("bio", e.target.value)}
        placeholder="Bio"
      />
      <pre>{JSON.stringify(user, null, 2)}</pre>
    </form>
  )
}

// Array state — add, remove, update items
function TodoApp() {
  const [todos, setTodos] = useState([])
  const [text, setText] = useState("")

  const add    = () => {
    if (!text.trim()) return
    setTodos(prev => [...prev, { id: Date.now(), text, done: false }])
    setText("")
  }
  const toggle = (id) => setTodos(prev =>
    prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
  )
  const remove = (id) => setTodos(prev => prev.filter(t => t.id !== id))

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)}
             onKeyDown={e => e.key === "Enter" && add()} />
      <button onClick={add}>Add</button>
      {todos.map(t => (
        <div key={t.id}>
          <input type="checkbox" checked={t.done} onChange={() => toggle(t.id)} />
          <span style={{ textDecoration: t.done ? "line-through" : "none" }}>{t.text}</span>
          <button onClick={() => remove(t.id)}>✕</button>
        </div>
      ))}
    </div>
  )
}

Common question

Can I change React state by assigning to it?

A component is a that takes props and returns what should be on screen. State is memory attached to that component between calls. useState returns the value for this render and a setter; calling the setter stores a new value and asks React to render again, which runs the function fresh with the new snapshot.

Three habits follow. Never mutate state; create a new value. Use the updater form when the next value depends on the current one. And keep state small: anything you can compute from existing state or props during render should be computed, not stored.

Source: The React documentation describes state as "a snapshot": setting it a new render with the new value and does not change the in the render that is already running.

Common mix-ups

Each of these sounds reasonable. Think about why it is wrong, then open the explanation.

  • State is a variable, so count = count + 1 should update it.

  • Pushing to a state array updates the component, because the array changed.

  • After calling the setter, the variable holds the new value on the next line.

Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.