Skip to content

Forms in React

React does not have a forms library built in. Forms are just controlled inputs, event handlers, and state — the same tools you already know. This lesson puts them together into a complete, production-quality form pattern.

A typical form in React has four parts:

  1. State for each field
  2. Controlled inputs bound to that state
  3. A submit handler that validates, acts, and resets
  4. Conditional error messages when validation fails
export default function TransactionForm({ categories, onAdd }) {
const [categoryId, setCategoryId] = useState('');
const [amount, setAmount] = useState('');
const [description, setDescription] = useState('');
const [date, setDate] = useState(new Date().toISOString().slice(0, 10));
const [error, setError] = useState('');
function handleSubmit(e) {
e.preventDefault();
if (!categoryId) { setError('Please select a category.'); return; }
if (!amount || parseFloat(amount) <= 0) { setError('Please enter a valid amount.'); return; }
setError('');
onAdd({
categoryId,
amount: parseFloat(amount),
description: description.trim(),
date,
});
setAmount('');
setDescription('');
// keep categoryId and date as convenience for rapid entry
}
return (
<section className="transaction-form card">
<h2>Add Transaction</h2>
{error && <p className="form-error">{error}</p>}
<form onSubmit={handleSubmit}>
<select value={categoryId} onChange={e => setCategoryId(e.target.value)}>
<option value="">Select category…</option>
{categories.map(c => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
<input type="number" value={amount} onChange={e => setAmount(e.target.value)} placeholder="0.00" min="0" step="0.01" />
<input type="text" value={description} onChange={e => setDescription(e.target.value)} placeholder="What was this for?" />
<input type="date" value={date} onChange={e => setDate(e.target.value)} />
<button type="submit">Add Transaction</button>
</form>
</section>
);
}

Validate on submit, not on every keystroke. Showing errors while the user is still typing is disruptive. Show the error once they try to submit with invalid data.

Keep validation close to the field it describes. For a large form, an error message object keyed by field name works well:

const [errors, setErrors] = useState({});
function validate() {
const e = {};
if (!categoryId) e.category = 'Required';
if (!amount) e.amount = 'Required';
return e;
}
function handleSubmit(e) {
e.preventDefault();
const errs = validate();
if (Object.keys(errs).length) { setErrors(errs); return; }
setErrors({});
// submit
}

After submitting, decide which fields to reset. In ZeroBudget’s transaction form, clear amount and description (likely different each transaction) but keep category and date (the user may add several transactions to the same category in sequence).

Reset everything when you want a completely fresh form. Keep values when you expect the user to add multiple similar entries quickly.

ZeroBudget also has a quick-add form on each CategoryCard. It is the same pattern with fewer fields:

function QuickAdd({ categoryId, onAdd }) {
const [amount, setAmount] = useState('');
const [description, setDescription] = useState('');
function handleSubmit(e) {
e.preventDefault();
if (!amount) return;
onAdd({ categoryId, amount: parseFloat(amount), description: description.trim() });
setAmount('');
setDescription('');
}
return (
<form onSubmit={handleSubmit} className="quick-add">
<input type="number" value={amount} onChange={e => setAmount(e.target.value)} placeholder="Amount" min="0" step="0.01" />
<input type="text" value={description} onChange={e => setDescription(e.target.value)} placeholder="Description" />
<button type="submit">+ Add</button>
</form>
);
}
  1. Build TransactionForm with the four fields above (category select, amount, description, date).
  2. Add validation that shows an error message if category or amount is missing on submit.
  3. On valid submit, call an onAdd prop with the transaction object and clear amount and description.
  4. In App.jsx, wire onAdd to a function that adds the transaction to a transactions state array and logs the array.
  • Forms in React combine controlled inputs, state, and a submit handler.
  • Validate on submit — not on every keystroke — to avoid disruptive error messages.
  • Clear only the fields that make sense to clear — keep values the user will likely reuse.
  • The quick-add pattern is the same form with fewer fields, co-located with the relevant component.