Skip to content

Controlled Inputs

There are two ways to handle form inputs in React: controlled and uncontrolled. Controlled inputs are the standard approach — React is the single source of truth for the input’s value.

An input is controlled when its value is set by React state and its onChange updates that state:

const [name, setName] = useState('');
<input
value={name}
onChange={e => setName(e.target.value)}
/>

The input cannot contain anything React did not put there. Every keystroke fires onChange, which calls setName, which triggers a re-render with the new value. The input shows exactly what is in state — always.

With an uncontrolled input, the DOM holds the value. To read it, you use a ref. The problem: the DOM and your state can drift. You might display a total based on old state while the input shows a new value the user typed. Controlled inputs prevent this — the input always reflects what React knows.

ZeroBudget calculates totals from state. If income amounts lived in the DOM instead of state, the totals would be wrong. Every input that feeds a calculation must be controlled.

Because state drives the value, clearing is just setting state:

function handleAdd(e) {
e.preventDefault();
if (!name.trim() || !amount) return;
addIncome({ name, amount: parseFloat(amount) });
setName('');
setAmount('');
}

Setting name to '' re-renders the input as empty. No DOM manipulation needed.

For numeric inputs, the state value is a string (that is what e.target.value gives you). Convert when you use it:

const [amount, setAmount] = useState('');
// When reading:
const total = parseFloat(amount) || 0;
// When saving:
addIncome({ amount: parseFloat(amount) });

Keeping the state as a string lets the input work naturally — users can type "1." while entering "1.50" without React snapping the value mid-edit.

<select> works the same way:

const [categoryId, setCategoryId] = useState('');
<select value={categoryId} onChange={e => setCategoryId(e.target.value)}>
<option value="">Select a category…</option>
{categories.map(c => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>

Checkboxes use checked instead of value:

const [agreed, setAgreed] = useState(false);
<input
type="checkbox"
checked={agreed}
onChange={e => setAgreed(e.target.checked)}
/>
  1. Add controlled name and amount inputs to IncomeSection. Both should reflect state and clear on valid submit.
  2. Add a controlled <select> to TransactionForm that lists your 8 default categories.
  3. Log the select’s current value to the console on form submit.
  4. Verify that changing the selection updates state and the select always shows what React put there.
  • A controlled input has value bound to state and onChange that updates state.
  • React is the single source of truth — the input cannot hold values React does not know about.
  • Clear inputs by setting state to the empty string.
  • Keep number inputs as strings in state; parse with parseFloat when you use the value.
  • Use checked (not value) for checkboxes and radios.