React / Components and state / Lesson 5
Controlled forms and validation boundaries
Coordinate text input, submission, and accessible feedback without confusing local form state with trusted application data.
On this page
Lesson goals
- Choose one owner for an input value
- A controlled field updates immediately
- Test editing and recovery paths
Choose one owner for an input value
A controlled text input receives its value from state and reports edits through onChange. Update that state from the current input value during the handler so typing remains responsive. An uncontrolled input instead lets the DOM own its current value and can provide an initial defaultValue. Do not accidentally switch between controlled and uncontrolled modes by passing undefined sometimes and a string later. Choose a consistent initial representation, commonly an empty string for editable text. This ownership decision does not determine where final validation happens; both approaches still need explicit submission and trust-boundary handling.
Keep input text separate from domain values
A numeric-looking field can contain transitional text while someone edits it. Converting every keystroke into a final number can destroy useful empty or partial states. Keep text where appropriate, parse at a deliberate boundary, and validate range, units, and requiredness independently. Browser type and constraint attributes improve interaction but do not establish server-side trust or authorization. A checkbox uses checked and the event target’s checked value, while text inputs use value. Labels still need htmlFor and a matching unique id, and an error needs a meaningful message and association beyond a colored border.
A controlled field updates immediately
The first example mirrors a name field into a greeting. It initializes a string and updates from event.target.value, so the controlled value remains consistent. The handler performs the local urgent edit directly rather than scheduling it as a Transition. A later lesson distinguishes transitions for less urgent rendering; they are not suitable for the state that directly controls typing. The greeting falls back visually when the name is empty without modifying the stored text. This keeps presentation policy separate from the exact value someone is editing and avoids rewriting the cursor’s input on every render.
import { useState } from "react";
export default function App() { const [name, setName] = useState(""); return <main><h1>Hello, {name || "Guest"}</h1><label htmlFor="name">Name</label><input id="name" value={name} onChange={e => setName(e.target.value)} /></main>; }
Initial: Hello, Guest. Typing Mira: Hello, Mira.
The stored value stays a string, and the visible greeting follows edits without an Effect.
Own the submit event and result
The second example prevents the browser’s default navigation for a synthetic local form, checks a simple quantity rule, and displays a result. It has no real endpoint and does not claim persistence. In a real application, the submit handler would coordinate pending state, server validation, uncertain outcomes, and duplicate-submission policy. Do not let an unrelated helper button submit accidentally; set button types explicitly. Keep an error’s aria-invalid state consistent with the visible message, and connect additional feedback through aria-describedby where useful. A success message should mean the operation succeeded under its actual contract, not merely that a click occurred.
import { useState } from "react";
export default function App() { const [text, setText] = useState(""); const [message, setMessage] = useState(""); function submit(e) { e.preventDefault(); const valid = /^[1-9][0-9]*$/.test(text); setMessage(valid ? "Quantity accepted locally" : "Enter a positive whole number"); } return <main><h1>Quantity</h1><form onSubmit={submit}><label htmlFor="qty">Quantity text</label><input id="qty" value={text} onChange={e => setText(e.target.value)} aria-describedby="result" /><button type="submit">Check</button><p id="result">{message}</p></form></main>; }
Submitting empty text: Enter a positive whole number. Submitting 3: Quantity accepted locally.
The demonstration validates a textual syntax at submit time and reports only local acceptance, with no network operation.
Test editing and recovery paths
Test empty input, ordinary text, pasted content, longer labels, invalid submission, correction, and keyboard operation. A form that rejects bad data but gives no route to repair it is incomplete. Preserve safe entered values after an error and move focus only under an intentional policy, such as a useful error summary. Server errors and offline conditions need feedback too. React can preserve field state across renders, but changing a component key or position can reset it, so identity choices matter. Inspect names and descriptions with errors present instead of assuming the default visual state covers the complete accessible form.
Common errors and fixes
Typing cannot change the field
- Why it happens
- A controlled value lacks a corresponding update.
- How to fix it
- Update value state from onChange or choose uncontrolled ownership.
- Prevent it next time
- Test actual typing, not only initial rendering.
A controlled-mode warning appears
- Why it happens
- The value changes from undefined to a string.
- How to fix it
- Initialize and preserve a consistent value type.
- Prevent it next time
- Test initial loading and cleared values.
A local success label falsely implies saving
- Why it happens
- Only client syntax checks occurred.
- How to fix it
- Describe local validation accurately and await real outcomes.
- Prevent it next time
- Separate validation and persistence states.
Check your understanding
1. Why might a quantity field store text even though the final domain value is an integer?
Show worked solution
Editing includes empty and partial states that are not final integers. Preserve the text for interaction, then parse and validate at submission or another deliberate boundary. This prevents a numeric coercion from silently converting empty text into zero or erasing an intermediate edit.
2. What should a submit button’s pending state communicate?
Show worked solution
It should indicate that the owned operation is in progress, prevent or safely reconcile duplicates according to policy, and preserve a recoverable failure path. It must not announce success before the operation is confirmed. Keep the button’s accessible name and focus behavior understandable during the state change.
Key takeaways
- Choose controlled or uncontrolled ownership deliberately.
- Editing text is not yet trusted domain data.
- Report actual outcomes with accessible feedback.