Course outline

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.

About 4 min readContent reviewed
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.

jsx Component module — requires the course React host
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>; }
Expected output
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.

jsx Component module — requires the course React host
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>; }
Expected output
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.

Official references