← React: components, state and interface decisions
04 / 10 · 60 MIN

Forms, DOM contracts and accessibility

Define who controls each field, associate labels with inputs and diagnose editing, submission and lost focus without confusing React state with DOM identity.

Choose value ownership

An uncontrolled input can receive defaultValue for its initial value; the browser then maintains the edited value. A controlled input receives value, or checked for a checkbox, and the application updates that state in response to editing. Choose according to the requirement: if another part of the interface must react to text on every change, controlled state can be appropriate.

Do not treat defaultValue as a permanent state connection. Also avoid inadvertently switching mode during the same input’s lifetime. A value starting undefined and later becoming a string mixes two contracts.

Initialize with an empty string or normalize with ?? when control is intended from the first render.

Synchronous updates and usable editing

In the email field, onChange reads event.target.value and immediately updates the state feeding value. If that setter is delayed with a timer, the old prop keeps controlling the input meanwhile and can interfere with editing. Delaying a remote search is a different responsibility from delaying representation of entered text.

Transforming text on every keystroke also deserves attention: always replacing it with uppercase can move the cursor when editing in the middle. If normalization is required, define its timing and preserve intended selection behavior. This lesson does not require implementing a complete input mask; it requires recognizing that controlled value and editing experience must remain consistent.

Checkboxes and read-only intent

A checkbox has checked state expressed by checked. event.target.value is the field value rather than that boolean. In the example, enabled receives event.target.checked and returns to the input through checked. If only an initial check is wanted without ongoing control, use defaultChecked.

A deliberately non-editable field should express that intent, for example with readOnly on an input receiving value. Adding an empty handler to suppress a warning does not create functional editing and can hide a product decision. The essential distinction is between an editable field with updates, an initially populated field and a field whose value must remain read-only.

Labels identifying the correct control

A label can contain the input or associate with it through matching htmlFor and id. In the example, email uses an explicit relationship and the checkbox uses nesting. Association lets the browser connect label activation to the control and provides programmatic identification relevant to assistive technology.

Visual proximity and matching className do not create that relationship. In a component reused twice, a repeated fixed id can produce an ambiguous association. useId generates the instance id for the label/input pair. This mechanism serves control association; list-item identity still needs stable data ids, as covered in the previous lesson.

Field name and submission

id identifies an element and can connect its label; name identifies the field in form data. Having id=email without name is not a replacement for that submission contract. A button inside form also needs clear intent: type=submit requests submission, while type=button supports its own action, such as preview.

In the example, onSubmit calls preventDefault and updates a local note. This avoids normal navigation but neither sends nor confirms remote data. If another implementation uses FormData, it should identify expected names and interpret the fields actually included.

Having a React handler does not automatically turn every visual element into submitted data.

When focus disappears

If controlled value updates correctly but focus disappears on every keystroke, examine subtree identity. A key generated with Math.random on each render makes React recreate the input. Defining one component inside another can also create a new type on every call and cause resets.

Renaming the state variable or removing labels does not address that cause. Separate two hypotheses: the same node received an inconsistent value, or the node was removed and recreated. The former points to the update contract; the latter points to identity and mounting.

This distinction guides investigation without attributing every focus loss to the mere use of onChange.

Evidence and example boundaries

The Recorded locally note describes only an in-memory update. It should not be interpreted as a service response, persistent storage or proof of complete validation. When reviewing a form, record who controls value, how changes return to state, how the label identifies the input and what action the button triggers.

If the page contains several reusable fields, check associations per instance. If remote work is involved, separately define requests, errors, cancellation and out-of-order responses. This lesson’s cases assess reading those contracts through choices and scenarios.

They require no code execution and do not establish a complete application accessibility audit.

import {useId, useState} from 'react';

export default function ContactDraft() {
  const emailId = useId();
  const [email, setEmail] = useState('');
  const [enabled, setEnabled] = useState(false);
  const [receipt, setReceipt] = useState('Nothing recorded locally');
  function recordLocally(event) {
    event.preventDefault();
    setReceipt(`Recorded locally: ${email}; enabled=${enabled}`);
  }
  return <form onSubmit={recordLocally}>
    <label htmlFor={emailId}>Contact email</label>
    <input id={emailId} name="email" type="email" value={email}
      onChange={event => setEmail(event.target.value)} />
    <label><input name="enabled" type="checkbox" checked={enabled}
      onChange={event => setEnabled(event.target.checked)} /> Enable contact</label>
    <button type="submit">Record locally</button>
    <p>{receipt}</p>
  </form>;
}
IN PRACTICE

email is always a string and enabled a boolean. The separate label uses the input’s id; the checkbox is nested inside its label. The handler cancels normal submission and records a local note. It does not demonstrate delivery, acceptance or validation by a server.

Common pitfalls

Using value without updating state; reading value from a checkbox when checked is intended; switching between undefined and a string; repeating ids; using new keys per keystroke; calling a local note server confirmation.

Related topics: State structure and component identity · Effects, refs and external synchronization · Testing behavior and evidence boundaries

Take this idea with you

A controlled field receives a consistent value and returns changes through its handler. Labels, ids, names and button types have separate contracts. Review should preserve editing and accurately describe the observed result.

Create account

Reference: Uncontrolled defaults · React 19.3.0; JSX