React · Build and review an expense interface · 40 MIN
Make input errors understandable
Validate a boundary and give the learner a way to recover.
Labels connect text to controls so people can navigate the form by keyboard and assistive technology. inputMode requests a suitable mobile keyboard but does not validate a value. This example parses a bounded decimal string into integer cents to avoid floating-point arithmetic during conversion. Errors use an alert and preserve the user’s input. The server must independently validate the same contract; disabling a button or checking input in a browser is not an authorization control. Keep error text specific enough to help a person correct it.
The interface is a projection of state. Make each transition explicit so you can explain it.
Read the example
'use client';
import {useState} from 'react';
export default function ExpenseForm({onAdd}:{onAdd:(expense:{label:string;cents:number})=>void}) {
const [error,setError] = useState('');
return <form onSubmit={e=>{
e.preventDefault(); const form=e.currentTarget, data=new FormData(form);
const label=String(data.get('label')??'').trim(), raw=String(data.get('amount')??'');
if(!label || label.length>80 || !/^\d{1,6}(\.\d{1,2})?$/.test(raw)) {setError('Use a label and a nonnegative amount with at most two decimal places.');return;}
const [whole,fraction='']=raw.split('.');
const cents=Number(whole)*100+Number(fraction.padEnd(2,'0'));
onAdd({label,cents}); form.reset();setError('');
}}>
<label htmlFor="label">Expense label</label><input id="label" name="label" required maxLength={80}/>
<label htmlFor="amount">Amount</label><input id="amount" name="amount" inputMode="decimal" required aria-describedby="amount-hint"/>
<p id="amount-hint">Use a decimal point, for example 12.50.</p>
{error&&<p role="alert">{error}</p>}<button type="submit">Add expense</button>
</form>;
}Check the expected output
Render this component in a React application; verify the checklist below.
Your challenge
Connect this form to the reducer from the previous unit. Confirm a new ID is assigned once per submission. Test keyboard navigation and malformed amounts.
Solution cost: Rendering grows with the number of visible items. Each reducer update below copies its collection. time · O(n) for n records, excluding framework internals. space
Common trap
A component rendering successfully does not prove its data or accessibility behavior.
Study the project implementation
'use client';
import {useState} from 'react';
export default function ExpenseForm({onAdd}:{onAdd:(expense:{label:string;cents:number})=>void}) {
const [error,setError] = useState('');
return <form onSubmit={e=>{
e.preventDefault(); const form=e.currentTarget, data=new FormData(form);
const label=String(data.get('label')??'').trim(), raw=String(data.get('amount')??'');
if(!label || label.length>80 || !/^\d{1,6}(\.\d{1,2})?$/.test(raw)) {setError('Use a label and a nonnegative amount with at most two decimal places.');return;}
const [whole,fraction='']=raw.split('.');
const cents=Number(whole)*100+Number(fraction.padEnd(2,'0'));
onAdd({label,cents}); form.reset();setError('');
}}>
<label htmlFor="label">Expense label</label><input id="label" name="label" required maxLength={80}/>
<label htmlFor="amount">Amount</label><input id="amount" name="amount" inputMode="decimal" required aria-describedby="amount-hint"/>
<p id="amount-hint">Use a decimal point, for example 12.50.</p>
{error&&<p role="alert">{error}</p>}<button type="submit">Add expense</button>
</form>;
}Further reading: Official React documentation
Next lesson: Project · Handle stale requests and failed responses →