CodingNeed.

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

Essential cookies keep your account signed in. Optional analytics is not configured on this site. Your choice does not affect access to lessons.

Read the Privacy Policy. You can change this choice in the footer.