Forms that people can trust
A stock adjustment form looks small, but every save changes what the shelf report says and what the store reorders. This lesson builds one that store managers can trust on a slow connection: it checks every rule on the server, explains each error so anyone can fix it, never saves the same adjustment twice, and shows the new count at once without lying when the server says no.
By the end you can
- Build a form that works as plain HTML before JavaScript loads, and explain why browser validation is only a hint
- Write a Server Function that checks the session, validates every rule and returns field errors through
useActionState - Show errors that people can find and fix with
aria-invalid,aria-describedby, an error summary and focus - Stop duplicate adjustments with a pending button and an idempotency key
- Update a row at once with
useOptimisticand let it roll back when the server rejects the change
The everyday picture
Think of a deposit slip at a bank counter, or a parcel form at a post office. The slip has a printed box for each thing the clerk needs, with a label next to it. A sign on the wall says "amount in numbers, sign at the bottom". The sign helps, but the clerk behind the glass checks every slip again, because anyone can write anything on paper. When something is wrong, a good clerk hands the slip back with a circle around the exact box and a short note: "the amounts in words and numbers do not match". Every slip has a printed serial number, so if the same slip is handed in twice, the clerk sees the number and does not book it twice. And some banks stamp your passbook "pending" while the clerk works, then cross the stamp out if the slip is refused.
A web form works the same way. Labels and the browser's own checks are the sign on the wall. The Server Function is the clerk, who checks everything again. Field errors are the circled boxes. The idempotency key is the serial number. The optimistic update is the pending stamp, and the rollback is the line through it.
Start with a plain HTML form
The back-office stock screen runs in stores at the edge of town, on old store PCs and cheap Android phones with patchy data. The page's JavaScript can take several seconds to arrive. A plain HTML <form> needs none of it: the browser itself collects the fields and sends them to the server with a POST request. So the first version of any form should work as HTML alone, and React adds comfort on top. This idea has a name: progressive enhancement.
Three things make a form field work. A <label> tied to the field (htmlFor in React, matching the field's id), so a tap on the label focuses the field and a screen reader reads the label when the field gets focus. A name, because the browser sends name and value pairs, and a field without a name is not sent at all. And the right type: type="number" for the change in stock, a <select> for the reason, a <textarea> for the note. On the server, the fields arrive as FormData, and every value is a string: the change arrives as "-24", and an empty field arrives as "".
The browser can also check some rules before it sends anything. required, min="-500", max="500" and step="1" make it block the submit and show a message in the user's own language, for free. Use them: they catch most typos at once. But treat them as a hint. An old browser may skip some checks, anyone can remove an attribute in the browser's developer tools, and a script can POST to the server without opening the page at all. Some rules cannot be written as attributes anyway: "a note is required for theft" depends on another field, and "the count cannot go below 0" depends on the stock in the database.
Quick check
Which stock adjustment rule can the browser check with HTML attributes alone?
min, max and step="1" on a number field cover the range and the whole-number rule. The note rule depends on another field, and the below-zero rule depends on the stock the server holds. Even the range check is only a hint: the server must check it again.