Skip to content

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.

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.

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.

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).

Never mutate state directly. React does not detect mutations — only replacements. Always call the setter with a new value:

// Wrong — mutates the existing value
count++;
// Correct — gives React a new value
setCount(count + 1);

For objects and arrays, always create a new one:

// Wrong
state.name = 'new name';
// Correct
setState({ ...state, name: 'new name' });

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.

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>
);
}
  1. Add useState to IncomeSection for name and amount inputs.
  2. Wire each input to its state value with value and onChange.
  3. Add a submit handler that logs the values to the console and clears the inputs on submit.
  4. 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.