JSTAcademy
0 XP
Dashboard
Crash Courses
useState & State Management
10 min
Basic+150 XP
Crash Courses · Basic

useState & State Management

Add interactivity to components with local state using the useState hook.
10 min read+150 XP on completionCert: React Crash Course
Tap any word in the text below to start reading from there.

useState & State Management

State is what makes a React component interactive. Without state, components are pure functions — same input always produces the same output. useState adds memory: a component can remember a value between renders and update the UI when that value changes.

Why Not Use a Regular Variable?

// This does NOT work — React does not know to re-render
function Counter() {
  let count = 0  // ← regular variable, lost on every render

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => { count++ }}>
        Increment
      </button>
    </div>
  )
}

When you click the button, count increments but React never re-renders because nothing told it state changed. You need useState — it signals React to re-render the component with the new value.

Basic useState

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)  // initial value = 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>
  )
}

Every call to setCount schedules a re-render. React re-runs the function, useState returns the new value, and React updates the DOM to match the new JSX.

Functional Updates — When State Depends on Previous

// RISKY — count might be stale in async or batched scenarios
setCount(count + 1)

// SAFE — functional update always receives the latest value
setCount(prev => prev + 1)

// When calling setCount multiple times in one handler:
function addThree() {
  setCount(prev => prev + 1)  // ← each sees the result of the previous
  setCount(prev => prev + 1)
  setCount(prev => prev + 1)
  // count increases by 3 — correct
}

Use the functional pattern whenever the new state depends on the old value.

State with Objects — Spread to Update

interface FormState {
  name: string
  email: string
  message: string
}

function ContactForm() {
  const [form, setForm] = useState<FormState>({
    name: '', email: '', message: ''
  })

  // Spread previous state, override only the changed field
  const handleChange = (field: keyof FormState) =>
    (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
      setForm(prev => ({ ...prev, [field]: e.target.value }))

  return (
    <form>
      <input value={form.name} onChange={handleChange('name')} placeholder="Name" />
      <input value={form.email} onChange={handleChange('email')} placeholder="Email" />
      <textarea value={form.message} onChange={handleChange('message')} />
    </form>
  )
}

Never mutate the state object directly (form.name = 'Jordan'). Always create a new object with the spread operator.

State with Arrays — Always Create a New Array

const [items, setItems] = useState<string[]>([])

// Add an item
setItems(prev => [...prev, newItem])

// Remove an item
setItems(prev => prev.filter(item => item !== targetItem))

// Update an item
setItems(prev => prev.map(item =>
  item.id === targetId ? { ...item, done: true } : item
))

Never use push(), splice(), or direct index assignment on state arrays. These mutate the existing array — React compares references, not deep values, so a mutated array still looks like the same array and the component does not re-render.

Derived State — Compute From State, Not Duplicate It

// Bad — two pieces of state that must always be in sync
const [items, setItems] = useState([...])
const [count, setCount] = useState(0)  // ← duplicate

// Good — derive count from items
const [items, setItems] = useState([...])
const count = items.length  // ← computed on every render, always correct

If a value can be calculated from existing state, compute it during render — do not store it as separate state.

0%