The useState Hook
Props let data flow into a component. But what about data that changes — a user’s input, a toggle, a count? That is state: data that lives inside a component and causes a re-render when it changes.
Calling useState
Section titled “Calling useState”useState is a function you call at the top of your component. It takes an initial value and returns a pair: the current value and a function to update it.
import { useState } from 'react';
function Counter() { const [count, setCount] = useState(0);
return ( <button onClick={() => setCount(count + 1)}> Count: {count} </button> );}count is the current value. setCount is the setter. When you call setCount(newValue), React updates the stored value and re-renders the component. The component function runs again with the new value.
The array destructuring convention
Section titled “The array destructuring convention”useState returns a two-element array. You destructure it into any names you like:
const [value, setValue] = useState(initialValue);const [name, setName] = useState('');const [isOpen, setIsOpen] = useState(false);The convention is [thing, setThing]. Stick to it — it makes code readable at a glance.
State is local
Section titled “State is local”Each component instance has its own state. Rendering <Counter /> three times gives you three independent counters. They do not share state unless you lift it up (covered in Lesson 04).
Updating state correctly
Section titled “Updating state correctly”Never mutate state directly. React does not detect mutations — only replacements. Always call the setter with a new value:
// Wrong — mutates the existing valuecount++;
// Correct — gives React a new valuesetCount(count + 1);For objects and arrays, always create a new one:
// Wrongstate.name = 'new name';
// CorrectsetState({ ...state, name: 'new name' });Functional updates
Section titled “Functional updates”When the new state depends on the previous state, use the functional form of the setter:
setCount(prev => prev + 1);This guarantees React uses the most recent value, even if multiple updates are batched. Use functional updates whenever you read the current value to compute the next one.
State in ZeroBudget
Section titled “State in ZeroBudget”The income form needs state for the input values:
export default function IncomeSection({ incomeSources }) { const [name, setName] = useState(''); const [amount, setAmount] = useState('');
function handleAdd(e) { e.preventDefault(); if (!name.trim() || !amount) return; // add to list — covered when we lift state up setName(''); setAmount(''); }
return ( <section className="income-section card"> <h2>Income</h2> <form onSubmit={handleAdd}> <input value={name} onChange={e => setName(e.target.value)} placeholder="Source name" /> <input type="number" value={amount} onChange={e => setAmount(e.target.value)} placeholder="Amount" /> <button type="submit">Add</button> </form> </section> );}Exercise
Section titled “Exercise”- Add
useStatetoIncomeSectionfornameandamountinputs. - Wire each input to its state value with
valueandonChange. - Add a submit handler that logs the values to the console and clears the inputs on submit.
- Confirm that typing in the fields updates the state and that submitting clears them.
useState(initialValue)returns[currentValue, setter].- Calling the setter updates the value and triggers a re-render.
- Never mutate state directly — always pass a new value to the setter.
- Use
setState(prev => newValue)when the new state depends on the previous one. - Each component instance has its own independent state.