Unit 4: Form Handling and Validation

INT252 — Web App Development With Reactjs 10 min read

I. Orientation — Forms as State, Interaction, and Data Boundaries

A React form is an interface for collecting user input, representing it as application data, validating it, and submitting it to another part of the system. Effective form handling depends on clear ownership of state, predictable submission workflows, accessible feedback, and separation between interface logic and validation rules.

  • Core data flow: Input generally moves from the user, through form controls, into React state or the browser’s DOM state, and then into a submission handler.
  • State ownership: A field may be controlled by React through value and onChange, or left under browser control and accessed through a reference.
  • Validation boundary: Client-side validation gives immediate feedback, but server-side validation remains necessary because browser data can be altered or bypassed.
  • Error representation: Errors are commonly stored by field name, such as { email: "Invalid email" }, so each message can be rendered beside its field.
  • Submission lifecycle: A practical form distinguishes states such as idle, submitting, success, and error.
  • Accessibility convention: Every control needs an accessible name, keyboard support, visible focus, and an understandable relationship to instructions and errors.
  • Schema principle: A schema describes valid data independently of the visual form, allowing the same rules to support parsing, validation, and type inference.

II. Component State Ownership — Browser-Controlled and React-Controlled Inputs

A. Controlled and uncontrolled components

Controlled and uncontrolled components differ according to whether React state or the DOM is the immediate source of a field’s current value.

  1. Controlled components:
    • Mechanism: React supplies the field’s value, while onChange updates state after each edit.
    • Concrete pattern:
JSX
const [name, setName] = useState("");

<input
  id="name"
  value={name}
  onChange={(event) => setName(event.target.value)}
/>
  • Identifiers: name is the current state value; setName updates it; event.target.value is the input’s latest text.
  • Advantages: Controlled fields support live validation, dependent fields, character counters, and immediate conditional rendering.
  • Cost: Every change causes a state update and typically a component render, so very large forms may require careful component separation.
  • Important rule: Avoid changing an input from uncontrolled to controlled. Initialize textual values with "", not undefined.
  1. Uncontrolled components:
    • Mechanism: The browser stores the live value, and React reads it through FormData or a ref.
    • Concrete pattern:
JSX
function submit(event) {
  event.preventDefault();
  const data = new FormData(event.currentTarget);
  console.log(data.get("name"));
}

<form onSubmit={submit}>
  <input name="name" defaultValue="" />
</form>
  • Identifiers: event.currentTarget is the form; FormData collects successful named controls; "name" matches the field’s name attribute.
  • Advantages: This approach reduces state code and suits simple forms or integration with non-React widgets.
  • Limitations: Live cross-field logic is harder because React does not automatically possess the latest value.
  • Required case: File inputs are normally uncontrolled because users, rather than application code, select their files.

III. Form Architecture — State, Submission, Reuse, and Interface Behaviour

A. Form state management patterns

Form state should represent values, interaction status, errors, and submission status without creating contradictory sources of truth.

  • Individual state variables: Small forms can use useState once per field, such as email and password; this becomes repetitive as fields increase.
  • Object state: Related values can be stored together and updated with computed property names.
JSX
const [values, setValues] = useState({ email: "", password: "" });

function handleChange(event) {
  const { name, value } = event.target;
  setValues(previous => ({ ...previous, [name]: value }));
}
  • Update meaning: previous is the existing object, and [name] updates the property whose key matches the input’s name.
  • Reducer pattern: useReducer is useful when events such as CHANGE, BLUR, RESET, and SUBMIT_FAILURE produce structured transitions.
  • Interaction metadata: touched.email records whether the field has been visited; dirty.email records whether its value differs from the initial value.
  • Derived state: Values such as isValid should usually be calculated from current values and errors rather than stored separately.

B. Handling form submission workflows

Submission is a staged process that prevents native navigation, validates data, performs an asynchronous operation, and reports the result.

  • Event handling: Attach one onSubmit handler to <form> so Enter-key submission and submit-button activation follow the same path.
  • Default behaviour: event.preventDefault() stops the browser from reloading or navigating before JavaScript processing completes.
  • Typical workflow:
    1. Read or use current values.
    2. Validate and stop if errors exist.
    3. Set status to submitting.
    4. Send data with fetch or another API client.
    5. Handle success or server errors.
    6. Restore a non-submitting state.
  • Duplicate prevention: Disable the submit button while pending, while preserving a visible label such as “Submitting…”.
  • Failure handling: Use try, catch, and finally; do not clear user-entered values after a recoverable network failure.
  • Server response mapping: A response such as { field: "email", message: "Already registered" } can be placed into the same field-error structure used by client validation.

C. Reusable form component design

Reusable form components should standardize structure and accessibility while allowing the parent form to own domain-specific data.

  • Composition: A FormField component can receive label, name, error, required, and children rather than assuming one input type.
  • Stable relationships: Generate or receive an id, connect <label htmlFor={id}>, and associate help text through aria-describedby.
  • Controlled interface: A reusable text input commonly accepts value, onChange, onBlur, and disabled.
  • Error interface: Prefer an error message or structured error object over an ambiguous Boolean such as hasProblem.
  • Extensibility: Forward ordinary attributes such as autoComplete, inputMode, and maxLength to the native control.
  • Boundary rule: Keep business rules—for example, a minimum order quantity—outside a generic visual input component.

D. Conditional rendering in form interfaces

Conditional rendering adapts a form to current values or workflow state without creating hidden inconsistencies.

  • Dependent fields: Render companyName only when accountType === "business".
  • Validation consequence: When a field disappears, decide explicitly whether to retain, clear, or exclude its value during submission.
  • Progressive disclosure: Reveal advanced options only when requested, reducing initial complexity while keeping controls discoverable.
  • Status feedback: Render a spinner during submission, a field message after validation, or a success panel after completion.
  • Avoid layout ambiguity: Do not hide required information solely with colour or unexplained icons.
  • State preservation: Conditional unmounting removes local component state; keeping a component mounted but visually hidden has different accessibility and state effects.

E. Accessibility considerations in form handling

Accessible forms expose names, instructions, states, and errors through both visual presentation and assistive technology.

  • Labels: Use an explicit <label htmlFor="email">Email</label> with <input id="email">; placeholder text is not a substitute.
  • Grouping: Use <fieldset> and <legend> for related radio buttons or checkboxes, such as a delivery-method group.
  • Error association: Connect an invalid field to its message using aria-describedby="email-error" and set aria-invalid="true".
  • Announcements: A submission summary may use role="alert" or an appropriate live region so new feedback is announced.
  • Keyboard operation: Preserve native controls and logical tab order; avoid positive tabIndex values that create unexpected navigation.
  • Focus management: After failed submission, move focus to the error summary or first invalid field without trapping the user.
  • Input assistance: Attributes such as autoComplete="email" and inputMode="numeric" help users enter expected data efficiently.

IV. Validation Models — Rules, Patterns, and Schemas

A. Client-side form validation concepts

Client-side validation checks browser-entered data before submission to improve feedback speed and data quality.

  • Constraint validation: Native attributes include required, min, max, minLength, maxLength, pattern, and semantic types such as email.
  • Validation timing: Validate on submit for completeness, on blur for timely field feedback, and on change only when immediate guidance is helpful.
  • Normalization: Trim surrounding whitespace or standardize case before validation when domain rules permit it.
  • Error quality: Messages should identify the problem and remedy, such as “Password must contain at least 12 characters.”
  • Security boundary: Client checks are advisory; the server must repeat authorization, integrity, and domain validation.
  • UX principle: Do not show an error before the user has interacted unless submission has already been attempted.

B. Commonly used validation patterns

Common validation patterns combine required checks, format checks, range checks, and relationships between multiple fields.

  • Presence: value.trim().length > 0 rejects an empty or whitespace-only name.
  • Length and range: A username may require 3–20 characters; an age may require a numeric value between defined limits.
  • Format: Prefer semantic input types and well-tested validators over overly restrictive regular expressions.
  • Confirmation: Password confirmation requires confirmPassword === password.
  • Cross-field rules: An end date must be on or after a start date.
  • Conditional requirement: companyName becomes required only when the account type is "business".
  • Asynchronous validation: Username availability requires a server request and should handle delay, cancellation, and stale responses.
  • Password caution: Strength rules should encourage length and compromised-password checks rather than arbitrary composition rules alone.

C. Schema-based validation principles

Schema-based validation represents the expected shape and constraints of submitted data in one declarative model.

  • Single definition: A schema can specify that email is a valid string and age is a positive integer.
  • Parsing: Schema libraries check unknown input and return validated data, often with transformations or coercion.
  • Type inference: In TypeScript, the schema can generate the corresponding static type, reducing mismatch between rules and interfaces.
  • Structured errors: Failures normally include paths such as ["address", "postcode"], enabling field-level error mapping.
  • Cross-field refinement: Object-level rules express relationships such as matching passwords.
  • Limit: A schema validates data, not UI interaction state such as whether a field is touched or whether focus should move.

V. Zod Integration — Typed Parsing and Error Handling

A. Form validation using Zod

Zod defines executable schemas that validate unknown data and provide typed results without throwing when safeParse is used.

JSX
import { z } from "zod";

const registrationSchema = z.object({
  email: z.string().trim().email("Enter a valid email"),
  password: z.string().min(12, "Use at least 12 characters"),
  confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
  path: ["confirmPassword"],
  message: "Passwords must match"
});
  • Object schema: z.object declares the required fields and their validators.
  • String rules: trim, email, and min(12) normalize or constrain string values.
  • Refinement: refine checks the relationship between two fields and assigns the failure to confirmPassword.
  • Safe parsing:
JSX
const result = registrationSchema.safeParse(values);
  • Result contract: result.success distinguishes valid and invalid outcomes; valid data is in result.data, while failures are in result.error.
  • TypeScript support: type Registration = z.infer<typeof registrationSchema> derives the validated data type from the schema.

B. Integrating schema validation with form logic and error handling

Schema integration connects parsing results to field messages, submission control, and server communication.

  • Submission gate: Call safeParse(values) inside onSubmit; return immediately when success is false.
  • Error flattening: result.error.flatten().fieldErrors groups messages by top-level field names.
  • Message mapping: Since each field may have multiple messages, an interface may display the first with fieldErrors.email?.[0].
  • Validated payload: Submit result.data, not the original values, because parsed data may be trimmed, transformed, or coerced.
  • Error separation: Keep field errors distinct from form-level errors such as “Service unavailable.”
  • State cleanup: Clear a field’s old error after successful revalidation, but retain unrelated server errors until their underlying values change.
  • Defence in depth: Reuse an equivalent schema on the server where possible, while separately enforcing authentication, authorization, and database constraints.