AiTechWorlds
AiTechWorlds
State is data a component owns and remembers between renders — change it, and the component redraws itself automatically. useState is the hook that gives a component this memory.
A thermostat is a good analogy. It doesn't just display the temperature once and freeze — it holds a target value in memory, and every time you turn the dial, the display updates and the system reacts. A React component without state is a printed thermometer: accurate once, then frozen forever.
State is data that:
Not state:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // [currentValue, setterFunction]
// ^— initial value
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>
);
}useState returns an array with exactly two items:
React detects state changes by reference equality. If you mutate the current value directly, React won't know it changed.
// WRONG — mutating state
const [user, setUser] = useState({ name: "Alice", age: 30 });
function birthday() {
user.age++; // Mutates the existing object
setUser(user); // React sees the same reference — no re-render!
}
// RIGHT — always create a new value
function birthday() {
setUser({ ...user, age: user.age + 1 }); // New object = new reference
}Same rule for arrays:
// WRONG
const [items, setItems] = useState(["a", "b", "c"]);
items.push("d"); // Mutates — won't trigger re-render
// RIGHT
setItems([...items, "d"]); // Add item
setItems(items.filter(i => i !== "b")); // Remove item
setItems(items.map(i => i === "a" ? "A" : i)); // Update itemWhen new state depends on previous state, use a function:
// This can have race conditions with multiple rapid clicks
setCount(count + 1);
// This is always correct — uses the latest state
setCount(prevCount => prevCount + 1);The function form guarantees you're working with the most recent state value:
function Counter() {
const [count, setCount] = useState(0);
function handleTripleIncrement() {
// These all run before re-render — count is still 0 for all three
setCount(count + 1); // → 1
setCount(count + 1); // → 1 again! count was still 0
setCount(count + 1); // → 1 again!
// Use functional update to correctly chain:
setCount(prev => prev + 1); // → 1
setCount(prev => prev + 1); // → 2
setCount(prev => prev + 1); // → 3
}
}function UserForm() {
const [form, setForm] = useState({
name: '',
email: '',
role: 'user',
});
function handleChange(field, value) {
setForm(prev => ({
...prev, // Keep all existing fields
[field]: value // Update only the changed field
}));
}
return (
<form>
<input
value={form.name}
onChange={e => handleChange('name', e.target.value)}
/>
<input
value={form.email}
onChange={e => handleChange('email', e.target.value)}
/>
<select
value={form.role}
onChange={e => handleChange('role', e.target.value)}
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</form>
);
}function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
function addTodo() {
if (!input.trim()) return;
setTodos(prev => [
...prev,
{ id: Date.now(), text: input.trim(), completed: false }
]);
setInput('');
}
function toggleTodo(id) {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}
function deleteTodo(id) {
setTodos(prev => prev.filter(todo => todo.id !== id));
}
return (
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addTodo()}
placeholder="Add a todo..."
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>×</button>
</li>
))}
</ul>
<p>{todos.filter(t => !t.completed).length} remaining</p>
</div>
);
}If the initial state is expensive to compute, pass a function:
// This runs getExpensiveValue() on every render — wasteful
const [data, setData] = useState(getExpensiveValue());
// This only runs once — React calls the function on mount only
const [data, setData] = useState(() => getExpensiveValue());
// Common use: read from localStorage
const [theme, setTheme] = useState(() => {
return localStorage.getItem('theme') ?? 'light';
});// Mistake 1: State too low — should be lifted
// Wrong: both siblings need the count, but only one has it
function Parent() {
return (
<>
<CounterDisplay /> {/* Can't see the count */}
<CounterButton /> {/* Has the count state */}
</>
);
}
// Correct: lift state to parent
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<CounterDisplay count={count} />
<CounterButton onIncrement={() => setCount(c => c + 1)} />
</>
);
}
// Mistake 2: Redundant state (derived data)
// Wrong: don't store what you can calculate
const [firstName, setFirstName] = useState('Alice');
const [lastName, setLastName] = useState('Smith');
const [fullName, setFullName] = useState('Alice Smith'); // Redundant!
// Correct: calculate it
const fullName = `${firstName} ${lastName}`; // No state neededNext lesson: useEffect — managing side effects like data fetching, subscriptions, and timers.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises