Volume I index
State and Events From Scratch

Form Inputs and Controlled Values

Ishtmeet Singh @ishtms/July 14, 2026/8 min read
#react#forms#controlled-inputs#state#accessibility

A browser input already knows how to accept text. React state becomes useful when the rest of the interface needs the current value during rendering.

jsx
function ReaderName() {
  const [name, setName] = useState("");

  return (
    <label>
      Reader name
      <input
        value={name}
        onChange={(event) => setName(event.target.value)}
      />
    </label>
  );
}

The input receives its displayed value from state. Every edit reports the next browser value through onChange. The handler requests that next state, React renders again, and the input receives the new value prop.

This is a controlled input. Its current value comes from React state.

Follow One Typed Character

The first render passes an empty string.

text
state name is ""
input receives value=""

The reader types R. The browser prepares the edited value and React calls the change handler.

text
event.target.value is "R"
handler calls setName("R")
React renders with name "R"
input receives value="R"

The cycle repeats for each edit.

React's onChange for text inputs runs as the value changes. It does not wait for the field to lose focus as the native DOM change event often does for text controls.

The handler must update the state synchronously from the new value. If it delays or transforms the wrong value, React can restore stale text on the next render.

Pair value with onChange

This input receives a fixed value and no update path.

jsx
<input value={name} />

Typing cannot persist because every render instructs the input to display name. React reports a read-only-field warning in development.

Add the handler.

jsx
<input
  value={name}
  onChange={(event) => setName(event.target.value)}
/>

Or declare an explicitly read-only input.

jsx
<input value={generatedId} readOnly />

Use readOnly when the user should select or copy a value but not edit it. Use plain text when the value does not need input semantics.

Initialize Controlled Text with a String

The value prop for a controlled text input should remain a string throughout the input's lifetime.

jsx
const [name, setName] = useState("");

This initialization is risky.

jsx
const [name, setName] = useState(undefined);

The first render leaves the input uncontrolled because no value is supplied. A later string turns it into a controlled input, and React reports the switch.

Normalize optional source data before supplying it.

jsx
const [name, setName] = useState(savedName ?? "");

Do not pass null as the controlled value. Use an empty string for an empty text field.

Uncontrolled Inputs Let the DOM Retain the Value

Use defaultValue for an initial value that the DOM will manage afterward.

jsx
<input name="readerName" defaultValue="Ada" />

React supplies Ada at initialization. Later typing remains in the DOM input without a state update for every character.

Changing the defaultValue prop after mount does not reset the current typed value. The prop describes the initial uncontrolled value.

Read uncontrolled form values on submission through FormData or through a ref for a specific imperative case.

jsx
function handleSubmit(event) {
  event.preventDefault();
  const data = new FormData(event.currentTarget);
  console.log(data.get("readerName"));
}

Controlled and uncontrolled inputs are both supported. Use controlled state when rendering, validation, or sibling UI needs the current value. Let the DOM manage it when the value is collected only at submission and no live React calculation depends on it.

Text Input Values Are Strings

The browser reports text-like input values as strings, including type="number" through event.target.value.

jsx
const [weeklyGoal, setWeeklyGoal] = useState("3");
jsx
<input
  type="number"
  value={weeklyGoal}
  onChange={(event) => setWeeklyGoal(event.target.value)}
/>

Keeping the raw string supports editing states such as an empty field, -, or a partially entered decimal when allowed. Converting every keystroke to a number can erase those temporary states.

Convert for a calculation.

jsx
const numericGoal = Number(weeklyGoal);
const validGoal = Number.isInteger(numericGoal) && numericGoal > 0;

event.target.valueAsNumber can read a number from a number input, but an empty or invalid value produces NaN. The component still needs an invalid-value policy.

Checkboxes Use checked

A checkbox expresses its current on or off state through checked, not value.

jsx
function CompletedFilter() {
  const [showCompleted, setShowCompleted] = useState(true);

  return (
    <label>
      <input
        type="checkbox"
        checked={showCompleted}
        onChange={(event) => setShowCompleted(event.target.checked)}
      />
      Show completed lessons
    </label>
  );
}

event.target.checked is a boolean. event.target.value is a separate form submission value and usually remains the string on unless another value prop is supplied.

For an uncontrolled checkbox, use defaultChecked.

jsx
<input type="checkbox" defaultChecked />

Do not use selected or value to control a checkbox's checked state.

Radio Buttons Share One Selected Value

A radio group represents one selection from several controls with the same name.

jsx
const [status, setStatus] = useState("all");
jsx
<label>
  <input
    type="radio"
    name="status"
    value="all"
    checked={status === "all"}
    onChange={(event) => setStatus(event.target.value)}
  />
  All lessons
</label>

Another control uses the same name and another value.

jsx
<input
  type="radio"
  name="status"
  value="complete"
  checked={status === "complete"}
  onChange={(event) => setStatus(event.target.value)}
/>

State stores the selected group value. Each radio calculates whether that value equals its own value.

The shared name also gives the browser the correct group behavior for keyboard selection and form submission.

Select Controls Use value on select

Control a select at the select element.

jsx
function ChapterFilter() {
  const [chapter, setChapter] = useState("all");

  return (
    <select
      value={chapter}
      onChange={(event) => setChapter(event.target.value)}
    >
      <option value="all">All chapters</option>
      <option value="jsx">JSX</option>
      <option value="state">State</option>
    </select>
  );
}

Do not set selected on an option in React source.

jsx
<option value="jsx" selected>JSX</option>

The select value determines which matching option is selected.

For multiple selection, add multiple and pass an array of selected option values.

jsx
<select multiple value={selectedChapters}>

Multiple-select change handling needs to read the selected options collection. The first app uses one selected value.

Textareas Also Use value

HTML source often places initial text inside a textarea. React controls it through value.

jsx
const [note, setNote] = useState("");
jsx
<textarea
  value={note}
  onChange={(event) => setNote(event.target.value)}
/>

Use defaultValue for an uncontrolled textarea.

jsx
<textarea defaultValue="Add a note" />

Do not place normal children inside a React textarea to establish its value.

File Inputs Remain User Controlled

The browser restricts assigning a file path to a file input. React cannot control its file selection through a normal value string.

jsx
<input type="file" onChange={handleFileChange} />

Read the selected FileList from the event.

jsx
function handleFileChange(event) {
  const file = event.target.files?.[0];
  if (!file) return;
  console.log(file.name);
}

The user and browser control the selected file value. Uploading, type verification, size limits, previews, and server handling are separate operations performed after selection.

Use Separate State for a Small Form

Three fields can have three direct state positions.

jsx
const [name, setName] = useState("");
const [weeklyGoal, setWeeklyGoal] = useState("3");
const [sendReminder, setSendReminder] = useState(false);

Each handler updates one value. The source makes the type and purpose of each state position visible.

An object can group fields when many operations update them together.

jsx
const [form, setForm] = useState({
  name: "",
  weeklyGoal: "3",
});

Update it by replacement.

jsx
setForm((current) => ({
  ...current,
  name: nextName,
}));

This replacement loses weeklyGoal.

jsx
setForm({ name: nextName });

React state setters do not merge objects automatically. The spread retains unchanged top-level fields.

For the milestone, separate state remains easier to inspect.

Submit Through the Form Event

Handle submission on the form.

jsx
function handleSubmit(event) {
  event.preventDefault();
  console.log({ name, weeklyGoal });
}
jsx
<form onSubmit={handleSubmit}>
  {/* labeled controls */}
  <button type="submit">Save goal</button>
</form>

The submit event includes button clicks and keyboard submission. preventDefault keeps this client-only example from performing a browser submission and document load.

Logging is a temporary milestone action. A production form needs a storage or server mutation path, pending state, error feedback, and server-side validation.

Preserve Browser Validation

Use native constraints where they represent the field.

jsx
<input
  id="email"
  name="email"
  type="email"
  required
  value={email}
  onChange={(event) => setEmail(event.target.value)}
/>

The browser can prevent invalid submission and provide platform feedback. React state can also calculate inline guidance.

jsx
const emailLooksValid = email.includes("@");

Client checks improve interaction but do not establish trust. Submitted values can bypass the browser and must be validated by the receiving server.

When showing a custom error, connect it to the field.

jsx
<input aria-describedby="email-error" aria-invalid={!emailLooksValid} />
<p id="email-error">Enter a complete email address.</p>

Render the error only when the product's validation timing says it should be shown. Reporting an error before the reader begins typing can create noisy form behavior.

Build the Reading Goal Form

Use controlled text and checkbox state.

jsx
function ReadingGoalForm() {
  const [name, setName] = useState("");
  const [weeklyGoal, setWeeklyGoal] = useState("3");
  const [sendReminder, setSendReminder] = useState(false);

  // return form
}

Each control receives its current state and change handler. The form submit handler reads the same render snapshot.

jsx
function handleSubmit(event) {
  event.preventDefault();
  console.log({ name, weeklyGoal, sendReminder });
}

Check the Form State

Each form control now reads one current value from state.

text
controlled text -> value string in React state
controlled checkbox -> checked boolean in React state
controlled radio group -> one selected value in React state
controlled select -> value on the select element
uncontrolled field -> current value retained by the DOM
file input -> selected files controlled by user and browser

Source Notes