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:
- The current value (
count) - A function to update it (
setCount)
Syntax
const [state, setState] = useState(initialValue);
state: the current valuesetState: function to update the valueinitialValue: 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
- useState returns
[value, setValue] - Never modify state directly, always use the setter
- Spread operator for objects and arrays:
{...obj},[...arr] - Functional form:
setState(prev => ...)when the new state depends on the old one - State is local to the component