Chapter 1: useState

The Problem

In plain JavaScript, you can modify a variable:

let count = 0;
count = count + 1;

But in React, this doesn't work:

function Counter() {
    let count = 0;

    const increment = () => {
        count = count + 1;  // The variable changes...
        console.log(count); // Displays 1, 2, 3...
    };

    // But the UI is NEVER updated!
    return (
        <div>
            <p>{count}</p>  {/* Always displays 0 */}
            <button onClick={increment}>+1</button>
        </div>
    );
}

Why? React doesn't know the variable has changed. You must tell it explicitly.

The Solution: useState

import { useState } from 'react';

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

    const increment = () => {
        setCount(count + 1);  // Tells React to re-render
    };

    return (
        <div>
            <p>{count}</p>
            <button onClick={increment}>+1</button>
        </div>
    );
}

useState returns an array with:

  1. The current value (count)
  2. A function to update it (setCount)

Syntax

const [state, setState] = useState(initialValue);
  • state: the current value
  • setState: function to update the value
  • initialValue: initial value (only once on first render)

Different State Types

Numbers

const [count, setCount] = useState(0);
setCount(count + 1);
setCount(count - 1);

Strings

const [name, setName] = useState('');
setName('John');
setName(e.target.value);  // For an input

Booleans

const [isOpen, setIsOpen] = useState(false);
setIsOpen(true);
setIsOpen(!isOpen);  // Toggle

Objects

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

// INCORRECT - direct mutation
user.name = 'John';  // Doesn't work!

// CORRECT - new object with spread
setUser({ ...user, name: 'John' });

Arrays

const [items, setItems] = useState([]);

// Add an element
setItems([...items, newItem]);

// Remove an element
setItems(items.filter(item => item.id !== idToRemove));

// Update an element
setItems(items.map(item =>
    item.id === idToUpdate ? { ...item, name: 'New name' } : item
));

Functional Update

When the new value depends on the previous one, use the functional form:

// POTENTIAL ISSUE with rapid calls
setCount(count + 1);
setCount(count + 1);  // count is always the old value!
// Result: +1 instead of +2

// CORRECT - functional form
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
// Result: +2

The functional form guarantees you're working with the latest value.

Multiple States in a Component

function LoginForm() {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(null);

    // ...
}

Or grouped in an object:

function LoginForm() {
    const [form, setForm] = useState({
        email: '',
        password: '',
        isLoading: false,
        error: null
    });

    const updateField = (field, value) => {
        setForm(prev => ({ ...prev, [field]: value }));
    };

    // updateField('email', 'john@example.com');
}

Lazy Initialization

If the initial value requires an expensive calculation, pass a function:

// PROBLEM - calculation runs on every render
const [data, setData] = useState(expensiveCalculation());

// CORRECT - calculation runs only once
const [data, setData] = useState(() => expensiveCalculation());

Comparison with PHP

Concept PHP (session) React (useState)
Storage $_SESSION['count'] = 0 useState(0)
Read $_SESSION['count'] count
Write $_SESSION['count']++ setCount(c => c + 1)
Persistence Server-side Browser memory

Exercises

Exercise 1: Counter with min/max

Create a counter that:

  • Cannot go below 0
  • Cannot exceed 10

Solution:

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

    const increment = () => {
        setCount(prev => Math.min(prev + 1, 10));
    };

    const decrement = () => {
        setCount(prev => Math.max(prev - 1, 0));
    };

    return (
        <div>
            <button onClick={decrement} disabled={count === 0}>-</button>
            <span>{count}</span>
            <button onClick={increment} disabled={count === 10}>+</button>
        </div>
    );
}

Exercise 2: Todo List

Create a component that:

  • Displays a list of todos
  • Allows adding a todo
  • Allows deleting a todo

Solution:

function TodoList() {
    const [todos, setTodos] = useState([]);
    const [input, setInput] = useState('');

    const addTodo = () => {
        if (input.trim()) {
            setTodos([...todos, { id: Date.now(), text: input }]);
            setInput('');
        }
    };

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

    return (
        <div>
            <input
                value={input}
                onChange={(e) => setInput(e.target.value)}
                onKeyPress={(e) => e.key === 'Enter' && addTodo()}
            />
            <button onClick={addTodo}>Add</button>

            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        {todo.text}
                        <button onClick={() => removeTodo(todo.id)}>×</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

Key Points to Remember

  1. useState returns [value, setValue]
  2. Never modify state directly, always use the setter
  3. Spread operator for objects and arrays: {...obj}, [...arr]
  4. Functional form: setState(prev => ...) when the new state depends on the old one
  5. State is local to the component

<- Back to module | Next chapter: useEffect ->