State & useState

Managing component state with the useState hook

What Is State?

State is data that changes over time in your component. Unlike props (which come from parent components), state is managed within the component itself. When state changes, React automatically re-renders the component to reflect those changes.

Why Do We Need State?

Regular variables don't trigger re-renders when they change. State is React's way of remembering values between renders and updating the UI when those values change.

The useState Hook

useState is a React Hook that lets you add state to functional components:

import { useState } from 'react';

function Counter() {
  // Declare a state variable called "count"
  // useState returns: [currentValue, setterFunction]
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

useState Syntax Breakdown

const [state, setState] = useState(initialValue);

// state       - The current value
// setState    - Function to update the value
// initialValue - Starting value (only used on first render)

πŸ“Œ Naming Convention

Use descriptive names: [value, setValue], [isOpen, setIsOpen], [users, setUsers]

πŸ”„ Re-rendering

Calling the setter function triggers a re-render with the new value

Different Types of State

import { useState } from 'react';

function Examples() {
  // String state
  const [name, setName] = useState('');
  
  // Number state
  const [count, setCount] = useState(0);
  
  // Boolean state
  const [isVisible, setIsVisible] = useState(false);
  
  // Array state
  const [items, setItems] = useState([]);
  
  // Object state
  const [user, setUser] = useState({ name: '', email: '' });
  
  // null/undefined initial state
  const [data, setData] = useState(null);

  return <div>...</div>;
}

Updating State Correctly

⚠️ Never Modify State Directly

Always use the setter function. Direct mutation won't trigger a re-render.

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

  // βœ— WRONG - Direct mutation
  const wrongIncrement = () => {
    count = count + 1;  // This won't work!
  };

  // βœ“ CORRECT - Use setter function
  const correctIncrement = () => {
    setCount(count + 1);
  };

  return <button onClick={correctIncrement}>{count}</button>;
}

Functional Updates

When the new state depends on the previous state, use a function:

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

  // βœ— May cause issues with batched updates
  const increment = () => {
    setCount(count + 1);
    setCount(count + 1);  // Still uses the old count!
  };

  // βœ“ CORRECT - Functional update
  const incrementCorrect = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);  // Works correctly!
  };

  // Practical example
  const addThree = () => {
    setCount(prevCount => prevCount + 3);
  };

  return <div>{count}</div>;
}

Updating Objects in State

Always create a new object when updating object state:

function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });

  // βœ— WRONG - Mutating existing object
  const updateNameWrong = (newName) => {
    user.name = newName;  // Direct mutation!
    setUser(user);        // Same reference, no re-render
  };

  // βœ“ CORRECT - Create new object with spread
  const updateName = (newName) => {
    setUser({
      ...user,           // Copy all existing properties
      name: newName      // Override the one you're changing
    });
  };

  // Update multiple fields
  const updateUser = (updates) => {
    setUser(prev => ({
      ...prev,
      ...updates
    }));
  };

  return (
    <input 
      value={user.name}
      onChange={(e) => updateName(e.target.value)}
    />
  );
}

Updating Arrays in State

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', done: false }
  ]);

  // Add item
  const addTodo = (text) => {
    setTodos([
      ...todos,
      { id: Date.now(), text, done: false }
    ]);
  };

  // Remove item
  const removeTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // Update item
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id 
        ? { ...todo, done: !todo.done }
        : todo
    ));
  };

  // Replace all items
  const clearAll = () => {
    setTodos([]);
  };

  return <div>...</div>;
}

πŸ’‘ Array Methods Reference

  • Add: [...arr, newItem] or [newItem, ...arr]
  • Remove: arr.filter(item => item.id !== id)
  • Update: arr.map(item => item.id === id ? {...item, ...changes} : item)

Multiple State Variables

function Form() {
  // Separate state for each value (recommended for unrelated data)
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errors, setErrors] = useState({});

  // OR group related state together
  const [formData, setFormData] = useState({
    name: '',
    email: ''
  });

  const [formState, setFormState] = useState({
    isSubmitting: false,
    errors: {}
  });

  return <form>...</form>;
}

Lazy Initial State

For expensive initial values, pass a function to useState:

// βœ— Runs on every render (expensive)
const [data, setData] = useState(expensiveCalculation());

// βœ“ Runs only on first render (lazy)
const [data, setData] = useState(() => expensiveCalculation());

// Practical example: Reading from localStorage
function App() {
  const [theme, setTheme] = useState(() => {
    const saved = localStorage.getItem('theme');
    return saved || 'light';
  });

  return <div className={theme}>...</div>;
}

Common Patterns

// Toggle boolean
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(prev => !prev);

// Counter with min/max
const [count, setCount] = useState(0);
const increment = () => setCount(prev => Math.min(prev + 1, 10));
const decrement = () => setCount(prev => Math.max(prev - 1, 0));

// Reset to initial value
const initialState = { name: '', email: '' };
const [form, setForm] = useState(initialState);
const resetForm = () => setForm(initialState);

🎯 useState Best Practices

  • βœ“ Keep state as minimal as possible
  • βœ“ Use functional updates when new state depends on old state
  • βœ“ Never mutate state directlyβ€”always create new objects/arrays
  • βœ“ Group related state, separate unrelated state
  • βœ“ Use lazy initialization for expensive computations
  • βœ“ Lift state up when multiple components need the same data