React useState Hook
Manage React component state with useState to store and update values across renders.
TL;DR
- 01Declare state with useState and get back value and setter.
- 02Call setter to update state, triggering a re-render.
- 03Use functional updates for state based on previous state.
Tips
- 01Use functional updates (prev => ...) when the new state depends on the old state — it's safer and more readable.
Warnings
- 01Never mutate state directly — always create new objects and arrays to trigger re-renders properly.
Basic useState
- Declare state with initial value.
const [count, setCount] = useState(0); - count is the current value, setCount updates and re-renders.
function Counter() { const [count, setCount] = useState(0); return ( <> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}> Increment </button> </> ); } - Each state variable needs its own useState call.
- Initialize state with strings, numbers, booleans, arrays, or objects.
const [name, setName] = useState(""); const [isOpen, setIsOpen] = useState(false); const [items, setItems] = useState([]); const [user, setUser] = useState(null); - Calling the setter always replaces the value, never merges.
// Calling setCount replaces the entire value setCount(42); // count is now 42, not 42 + oldCount
Multiple State Variables
- Declare multiple state variables separately.
function Form() { const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [age, setAge] = useState(0); return ( <> <input value={name} onChange={(e) => setName(e.target.value)} /> <input value={email} onChange={(e) => setEmail(e.target.value)} /> <input value={age} onChange={(e) => setAge(e.target.value)} /> </> ); } - Group related state into a single object.
const [form, setForm] = useState({ name: "", email: "", age: 0 }); - Update a single field in an object with spread syntax.
const handleChange = (field) => (e) => { setForm(prev => ({ ...prev, [field]: e.target.value })); }; - Keep unrelated state variables separate for clarity.
// These are unrelated — keep them separate const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [data, setData] = useState(null); - Use useReducer when multiple variables always update together.
// If isLoading, error, and data always change at once // consider useReducer instead of three useState calls
Functional Updates
- Use a function to update state based on previous value.
function Counter() { const [count, setCount] = useState(0); // Direct update (fine for simple cases) // setCount(count + 1); // Functional update (preferred for dependent updates) return <button onClick={() => setCount(prev => prev + 1)}>Increment</button>; } - Functional updates are safer when multiple updates happen fast.
const handleClick = () => { setCount(prev => prev + 1); setCount(prev => prev + 1); // Both increments use the latest state — count increases by 2 }; - Use functional updates inside useEffect and useCallback.
useEffect(() => { const timer = setInterval(() => { setCount(prev => prev + 1); // safe inside interval }, 1000); return () => clearInterval(timer); }, []); // no count in deps needed - Functional updates work with arrays too.
function addItem(newItem) { setItems(prev => [...prev, newItem]); } function removeItem(id) { setItems(prev => prev.filter(item => item.id !== id)); } - Direct updates are fine when the new value doesn't depend on old state.
// setName doesn't need the previous name setName("Alice"); setIsOpen(true);
Lazy Initialization
- Initialize state from an expensive computation using a function.
const [state, setState] = useState(() => { return expensiveComputation(); }); - Function runs only on mount, not on every render.
const [todos, setTodos] = useState(() => { const saved = localStorage.getItem("todos"); return saved ? JSON.parse(saved) : []; }); - Useful for loading from localStorage or parsing initial data.
- Without lazy initialization, the function runs on every render.
// Bad: expensiveComputation() runs on every render const [state, setState] = useState(expensiveComputation()); // Good: arrow function defers the call to mount only const [state, setState] = useState(() => expensiveComputation()); - Pass the function itself, not the result of calling it.
// Correct: pass a function useState(() => computeInitialValue()) // Wrong: calls the function immediately useState(computeInitialValue())
Updating Objects and Arrays
- Create a new object when updating — never mutate state directly.
const [user, setUser] = useState({ name: "Alice", age: 30 }); // Wrong: mutation — React won't detect the change user.age = 31; // Right: create new object with spread setUser({ ...user, age: 31 }); - Create a new array for all array state operations.
const [items, setItems] = useState(["a", "b", "c"]); // Add item setItems([...items, "d"]); // Remove item setItems(items.filter(item => item !== "b")); - Update a nested object by spreading at every level.
const [profile, setProfile] = useState({ name: "Alice", address: { city: "NYC" } }); // Update nested field setProfile(prev => ({ ...prev, address: { ...prev.address, city: "LA" } })); - Update a specific item in an array by mapping over it.
const [tasks, setTasks] = useState([{ id: 1, done: false }]); function toggleTask(id) { setTasks(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t) ); } - Use the Immer library for complex nested updates.
import produce from "immer"; setProfile(produce(draft => { draft.address.city = "LA"; // direct mutation is safe inside produce }));
FAQ
Use functional updates whenever the new state depends on the previous state, especially inside event handlers, async callbacks, or effects where the closure may capture a stale value. This guarantees you're working with the latest state, not a snapshot from when the function was created.
Spread the existing object and override only the changed fields: setState(prev => ({ ...prev, name: 'new' })). React requires a new object reference to detect the change, so never do setState(obj.name = 'new') directly.
Yes — declare as many useState calls as you need, one per logical piece of state. Keeping state variables separate (e.g., const [name, setName] and const [age, setAge]) is cleaner and avoids the need to spread on every update compared to storing everything in one object.
Passing a function to useState — useState(() => expensiveCalc()) — tells React to call it only on the initial render instead of every render. Use this when the initial value requires heavy computation, parsing, or reading from localStorage.
Calling array.push() mutates the existing array, so React sees the same reference and skips the re-render. Instead, create a new array: setState(prev => [...prev, newItem]).