RE

useState

React · 6 entries

Basic Usage

syntax
const [state, setState] = useState(initialValue);
example
import { useState } from 'react';

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}
output
Displays count starting at 0. Clicking increment adds 1 each time.

Note setState triggers a re-render. The state value remains the same within the current render cycle even after calling setState -- the new value appears on the next render.

Updater Function

syntax
setState(prevState => newState);
example
function StepCounter() {
  const [count, setCount] = useState(0);

  function incrementThree() {
    // Each updater receives the latest pending state
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  }

  return (
    <button onClick={incrementThree}>
      Count: {count}
    </button>
  );
}
output
Each click adds 3 to the count, because each updater builds on the previous pending value.

Note Always use the updater form when the next state depends on the previous state. Writing setCount(count + 1) three times in a row would only add 1 because count is a stale snapshot from the current render.

Lazy Initialization

syntax
const [state, setState] = useState(() => computeInitialState());
example
function FilteredList() {
  // Expensive computation runs only on mount, not every render
  const [sortedItems, setSortedItems] = useState(() => {
    const saved = localStorage.getItem('cachedItems');
    return saved ? JSON.parse(saved) : [];
  });

  return <ItemList items={sortedItems} />;
}

Note Pass a function (not the result of calling it) to avoid running expensive initialization logic on every render. React only calls the initializer function during the first render and ignores it on subsequent renders.

Objects in State

syntax
setState(prev => ({ ...prev, key: newValue }));
example
function ProfileEditor() {
  const [profile, setProfile] = useState({
    firstName: '',
    lastName: '',
    bio: '',
  });

  function handleChange(field, value) {
    setProfile(prev => ({
      ...prev,
      [field]: value,
    }));
  }

  return (
    <>
      <input
        value={profile.firstName}
        onChange={e => handleChange('firstName', e.target.value)}
        placeholder="First name"
      />
      <input
        value={profile.lastName}
        onChange={e => handleChange('lastName', e.target.value)}
        placeholder="Last name"
      />
    </>
  );
}

Note Always spread the previous object to create a new copy. Directly mutating state (profile.firstName = 'X') will NOT trigger a re-render because React compares by reference.

Arrays in State

syntax
// Add
setItems(prev => [...prev, newItem]);
// Remove
setItems(prev => prev.filter(i => i.id !== id));
// Update one
setItems(prev => prev.map(i => i.id === id ? { ...i, done: true } : i));
example
function TodoList() {
  const [todos, setTodos] = useState([]);
  const [text, setText] = useState('');

  function addTodo() {
    if (!text.trim()) return;
    setTodos(prev => [
      ...prev,
      { id: crypto.randomUUID(), text, done: false },
    ]);
    setText('');
  }

  function removeTodo(id) {
    setTodos(prev => prev.filter(t => t.id !== id));
  }

  function toggleTodo(id) {
    setTodos(prev =>
      prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
    );
  }

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map(t => (
          <li key={t.id}>
            <span
              style={{ textDecoration: t.done ? 'line-through' : 'none' }}
              onClick={() => toggleTodo(t.id)}
            >
              {t.text}
            </span>
            <button onClick={() => removeTodo(t.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Note Use filter to remove, map to update, and spread to add. Never use push, splice, or direct index assignment on state arrays -- those mutate the existing array instead of creating a new one.

Multiple State Variables

syntax
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [isActive, setIsActive] = useState(false);
example
function RegistrationForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [acceptTerms, setAcceptTerms] = useState(false);

  const isValid = email.includes('@') && password.length >= 8 && acceptTerms;

  return (
    <form>
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={acceptTerms}
          onChange={e => setAcceptTerms(e.target.checked)}
        />
        I accept the terms
      </label>
      <button disabled={!isValid}>Register</button>
    </form>
  );
}

Note Separate state variables are preferred when values change independently. Combine into an object only when values logically belong together and always change at the same time.