Frontend Frameworks (React.js/Vue.js)

Chapter 5
Interaction Design, Forms, and Validation

Bachelors Program in Full Stack Development
Frontend Frameworks (React.js/Vue.js)   •   Bachelor

Interaction Design, Forms, and Validation

Introduction

Forms are one of the principal points at which users and web applications exchange information. Registration, authentication, search, checkout, booking, assessment, and administrative systems all depend on reliable interaction design. A technically correct form can still fail if users cannot understand what is required, if errors are discovered too late, or if entered data disappears unexpectedly.

In React.js and Vue.js, form development combines three concerns: the browser’s native input behaviour, the framework’s state-management model, and the user experience created by feedback and accessibility decisions. This chapter explains controlled and uncontrolled inputs, React’s one-way data flow, Vue’s two-way binding, event handling, complex forms, validation, file inputs, reusable abstractions, and accessible interaction patterns.

Learning Objectives

By the end of this chapter, students should be able to:

  • distinguish controlled and uncontrolled form inputs;
  • implement event handling in React and Vue;
  • explain React state updates and Vue’s v-model binding;
  • design forms containing nested, repeated, conditional, and asynchronous fields;
  • implement synchronous and server-side validation;
  • provide clear, accessible error feedback;
  • handle file inputs safely;
  • create reusable form components and composables/hooks; and
  • evaluate form designs for usability, predictability, security, and accessibility.

1. Interaction Design as a State Problem

A form is not merely a collection of HTML controls. It is a state machine. At any moment, a form has values, interaction state, validation state, submission state, and possibly server responses.

A useful conceptual model is:

State categoryExamples
ValuesName, email, selected plan
InteractionFocused, touched, dirty, visited
ValidationValid, invalid, pending, unknown
SubmissionIdle, submitting, succeeded, failed
External dataExisting profile, server error, upload progress

The design goal is predictability: a user should understand what the interface currently believes, what remains to be done, and what will happen after submission. State should have a single authoritative source wherever possible. Duplicating the same value in several state variables creates synchronization bugs.

The browser already supplies useful behaviour: labels, keyboard navigation, native constraint validation, focus management, and input semantics. Framework code should enhance this behaviour rather than unnecessarily replace it.

2. HTML Foundations and Event Handling

A well-designed framework form begins with semantic HTML. A control should have a visible or programmatically associated label, an appropriate input type, a name, and useful autocomplete metadata.

<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email" required>

The for attribute of the label must match the input’s id. Placeholder text is not a replacement for a label because it disappears while the user types and may have insufficient contrast.

In React, event handlers use camelCase and receive a synthetic event whose interface follows the browser event model:

function SearchBox() {
  const [query, setQuery] = useState('');

  function handleChange(event) {
    setQuery(event.target.value);
  }

  function handleSubmit(event) {
    event.preventDefault();
    // submit query to the application
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="query">Search</label>
      <input
        id="query"
        value={query}
        onChange={handleChange}
      />
      <button type="submit">Search</button>
    </form>
  );
}

In Vue, handlers are commonly expressed with directives:

<script setup>
import { ref } from 'vue'
const query = ref('')
function submit() {
  // submit query to the application
}
</script>

<template>
  <form @submit.prevent="submit">
    <label for="query">Search</label>
    <input id="query" v-model="query" />
    <button type="submit">Search</button>
  </form>
</template>

The .prevent modifier calls preventDefault(). Other useful Vue modifiers include .stop, . once, .trim, and .number; they should be used only when their behaviour is intentional and understandable.

Events should normally update state through a small, named handler. This makes transformations, validation, analytics, and testing easier to reason about. A handler should not silently perform unrelated side effects.

3. Controlled and Uncontrolled Inputs

A controlled input receives its current value from application state and reports changes through an event. The framework is therefore the source of truth. In React:

function ProfileName() {
  const [name, setName] = useState('');
  return (
    <input
      value={name}
      onChange={event => setName(event.target.value)}
    />
  );
}

Controlled inputs make values available for conditional rendering, live validation, formatting, undo, and coordinated fields. Their disadvantages are additional code and the possibility of unnecessary re-rendering in very large forms.

An uncontrolled input keeps its current value in the DOM. React may provide an initial value with defaultValue, and a ref can read the value when needed:

function QuickSearch() {
  const inputRef = useRef(null);
  function submit(event) {
    event.preventDefault();
    console.log(inputRef.current.value);
  }
  return (
    <form onSubmit={submit}>
      <input ref={inputRef} defaultValue="" />
      <button>Search</button>
    </form>
  );
}

Uncontrolled inputs can be suitable for simple forms, integration with non-React libraries, and large forms where every keystroke need not enter component state. However, they make cross-field rules and immediate feedback less direct.

A React input should not switch between controlled and uncontrolled modes during its lifetime. For example, initialise text state to '', not undefined, when the input will later receive a string. Vue’s v-model provides a convenient abstraction over the relevant value and input events, but the same conceptual decision remains: determine where the authoritative value lives.

Special Controls

Checkboxes use checked, not value, to represent their Boolean state in React:

<input
  type="checkbox"
  checked={accepted}
  onChange={event => setAccepted(event.target.checked)}
/>

Select controls use value and option values. Radio buttons share a name and represent one choice. For a multiple select, the state should be a collection of selected values. In Vue, v-model handles these distinctions automatically according to input type.

File inputs are a deliberate exception. Browsers do not permit a web application to assign an arbitrary local file path to an input for security reasons. The selected File objects are read from event.target.files; the input itself is effectively uncontrolled.

4. React State and Vue Two-Way Binding

React promotes one-way data flow: data moves from parent to child through props, and children communicate changes through callback functions. This makes ownership explicit and supports predictable reasoning.

function Quantity({ value, onChange }) {
  return (
    <input
      type="number"
      min="1"
      value={value}
      onChange={event => onChange(Number(event.target.value))}
    />
  );
}

The child does not directly mutate the parent’s state. It requests a change through onChange.

Vue’s v-model expresses a two-way binding at the component boundary. On a native input it connects a value to an input event; on a custom component it conventionally corresponds to a modelValue prop and an update:modelValue event:

<script setup>
defineProps({ modelValue: String })
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

Two-way binding is convenient, but it can obscure ownership if many components mutate shared state. A robust design documents which component owns the value and uses explicit events for meaningful domain changes.

5. Managing Complex Forms

A complex form may contain nested objects, repeated rows, conditional sections, and fields whose choices depend on earlier answers. A useful state structure should mirror the domain without storing values that can be derived.

const [form, setForm] = useState({
  customer: { name: '', email: '' },
  delivery: { method: 'standard', address: '' },
  items: [{ productId: '', quantity: 1 }]
});

Updates must preserve unaffected branches:

setForm(previous => ({
  ...previous,
  customer: { ...previous.customer, name: nextName }
}));

Functional updates are important when a new state depends on previous state. Mutating previous.customer.name directly can prevent reliable change detection and create difficult-to-reproduce bugs.

For many fields, useReducer makes transitions explicit:

function reducer(state, action) {
  switch (action.type) {
    case 'fieldChanged':
      return { ...state, [action.name]: action.value };
    case 'reset':
      return action.initial;
    default:
      return state;
  }
}

Repeated fields require stable keys. A database identifier or generated item identifier is preferable to an array index, especially when rows can be inserted or removed. Otherwise, focus and field values can appear to move to the wrong row.

Conditional fields should have a clear policy. If a hidden field is no longer relevant, its value may need to be cleared or excluded from submission. Merely applying CSS visibility does not remove it from the accessibility tree or validation logic. Use conditional rendering when the field genuinely does not exist in the current task.

6. Validation as a Layered Process

Validation has several distinct purposes:

  1. Type and constraint validation: Is the value syntactically acceptable?
  2. Domain validation: Is it meaningful for the business rule?
  3. Cross-field validation: Do related values agree?
  4. Server validation: Does the authoritative system accept it?

Client-side validation improves responsiveness but cannot establish trust. Any value sent to a server must be validated and authorized again on the server.

A validation function should preferably be pure: given the same input, it returns the same result without changing application state or making network requests.

function validate(values) {
  const errors = {};
  if (!values.name.trim()) errors.name = 'Enter your name.';
  if (!values.email.includes('@')) errors.email = 'Enter a valid email address.';
  if (values.password.length < 12) {
    errors.password = 'Use at least 12 characters.';
  }
  if (values.password !== values.confirmPassword) {
    errors.confirmPassword = 'Passwords do not match.';
  }
  return errors;
}

The exact email rule should be appropriate to the application; simplistic regular expressions often reject legitimate addresses. Native constraints such as required, minLength, maxLength, min, max, and type="email" provide useful baseline checks, but they do not replace domain validation.

Validation timing affects user experience. Validating every keystroke can produce premature errors, particularly when a user has entered only part of a value. A common policy is to validate on blur, then validate on change after a field has been touched. On submit, validate all relevant fields and move focus to the first invalid control.

Errors should be associated with their controls:

const errorId = 'email-error';
<input
  id="email"
  aria-invalid={Boolean(errors.email)}
  aria-describedby={errors.email ? errorId : undefined}
/>
{errors.email && <p id={errorId} role="alert">{errors.email}</p>}

Error messages should state what is wrong and how to correct it. “Invalid input” is less useful than “Enter a date in the format DD/MM/YYYY.” Avoid relying on colour alone; combine text, icons where appropriate, and programmatic relationships.

7. Submission, Asynchronous Validation, and Security

Submission is an asynchronous state transition. The interface should prevent accidental duplicate submissions, communicate progress, and restore an actionable state after success or failure.

async function handleSubmit(event) {
  event.preventDefault();
  const nextErrors = validate(form);
  setErrors(nextErrors);
  if (Object.keys(nextErrors).length) return;

  setStatus('submitting');
  try {
    await saveForm(form);
    setStatus('success');
  } catch (error) {
    setStatus('error');
    setFormError('We could not save your information. Try again.');
  }
}

A server may return field-specific errors, a general error, or both. Preserve entered values whenever safe, because forcing users to re-enter data increases abandonment. Do not expose stack traces, database details, or sensitive diagnostic information.

Security responsibilities include output encoding, server-side validation, authorization, CSRF protection where applicable, secure transport, rate limiting, and careful handling of personal data. Passwords should never be logged or echoed in error messages. Client-side validation is an interface feature, not a security boundary.

8. File Inputs and Uploads

A file input provides one or more File objects containing metadata such as name, type, and size. A typical React handler is:

function FileField({ onSelect }) {
  function handleChange(event) {
    const file = event.target.files?.[0];
    if (!file) return;
    onSelect(file);
  }
  return <input type="file" accept="image/png,image/jpeg" onChange={handleChange} />;
}

The accept attribute guides the file chooser but is not a security control. The server must inspect file size, content type, file signature, permissions, and storage policy. User-supplied filenames should not become unrestricted filesystem paths. Uploads should be scanned and stored outside executable application directories when appropriate.

For previews, URL.createObjectURL(file) may be used, but the URL should later be released with URL.revokeObjectURL() to avoid retaining browser resources. Do not display an uploaded file as trusted HTML merely because its extension appears safe.

9. Reusable Form Abstractions

Reusable abstractions should remove repetition without hiding important behaviour. Useful components include Field, FieldGroup, ErrorMessage, SubmitButton, and domain-specific controls such as DateRangePicker.

A reusable field component can standardise label association and error semantics:

function Field({ id, label, error, children }) {
  const errorId = `${id}-error`;
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      {children({
        id,
        'aria-invalid': Boolean(error),
        'aria-describedby': error ? errorId : undefined
      })}
      {error && <p id={errorId} role="alert">{error}</p>}
    </div>
  );
}

Hooks and composables centralise form behaviour. A React hook might expose values, errors, handleChange, handleBlur, handleSubmit, and reset. A Vue composable might expose refs and functions with equivalent responsibilities. The abstraction should define clear contracts: value shape, validation timing, submission return behaviour, reset semantics, and error format.

Avoid building a universal form abstraction so complex that developers cannot predict its behaviour. Native HTML semantics and ordinary component composition are often preferable to a highly magical API.

10. Accessibility and Inclusive Interaction

Accessible forms are easier for everyone to use. Important practices include:

  • use semantic controls rather than clickable div elements;
  • provide visible labels and instructions before the control;
  • maintain logical keyboard order and visible focus indicators;
  • ensure sufficient colour contrast;
  • associate errors with controls using aria-describedby;
  • use aria-invalid only when the value is actually invalid;
  • provide a summary for multiple errors and link each item to its field;
  • announce important asynchronous status changes appropriately; and
  • do not require precise pointer movement when keyboard or assistive technology is expected.

Do not move focus unexpectedly during ordinary typing. After a failed submission, focusing the first invalid field is usually helpful, while a successful submission may focus a confirmation message if the page context changes. Test with keyboard-only navigation and at least one screen reader, in addition to automated tools.

11. Illustrative Case: Course Registration Form

Background

A university builds a course-registration form containing student details, a delivery mode, selected modules, an optional supporting document, and a declaration.

Problem

The first version validates only when the server responds. Students receive a page-level message, lose focus position, and sometimes submit duplicate requests. A hidden address field is still sent when “online” delivery is selected.

Analysis

The form needs explicit state for values, touched fields, errors, and submission status. Module rows require stable identifiers. Delivery mode determines whether the address exists. The file is optional but must satisfy size and type rules. The server remains authoritative for enrolment eligibility and module capacity.

Approach

The team uses controlled values for ordinary fields and a reducer for complex transitions. Validation occurs on blur after interaction and for all fields on submit. A summary appears at the top after a failed submission, with links to field errors. The address section is conditionally rendered and its value is cleared when online delivery is selected. The submit button is disabled while submission is pending, but the server also rejects duplicate request identifiers. The upload is checked in the browser for immediate feedback and again on the server.

Outcome

Students receive local, actionable feedback without waiting for the network. Server errors are mapped to fields where possible. The form preserves values after recoverable failures and announces successful registration. Keyboard testing identifies and corrects a missing focus style.

Learning Points

The case illustrates that validation, accessibility, state ownership, and security are connected. A form is successful not merely when it accepts valid data, but when users can understand, correct, and safely submit that data.

12. Limitations and Contemporary Practice

Framework abstractions do not eliminate browser differences, network failures, or difficult domain rules. Large forms may require performance profiling, field-level subscriptions, or deferred validation. Internationalisation complicates dates, numbers, names, address formats, and error messages. Validation libraries can improve consistency, but their schemas must reflect actual business rules and should not be accepted without review.

Modern applications increasingly combine server actions, progressive enhancement, schema-based validation, and accessible component libraries. AI-assisted code generation can produce handlers and schemas quickly, but generated code must be reviewed for unsafe file handling, incorrect assumptions about validation, missing labels, and accidental leakage of personal data. Automated tools complement, rather than replace, user testing and expert accessibility review.

Best-Practice Checklist

Before releasing a form, verify that:

  • every control has a label and a stable name;
  • state ownership is clear;
  • controlled inputs are initialised consistently;
  • repeated items have stable keys;
  • validation messages explain corrective action;
  • server validation and authorization are present;
  • pending, success, and failure states are visible;
  • duplicate submissions are handled;
  • file uploads are checked and safely stored;
  • errors are accessible by keyboard and assistive technology; and
  • entered information is preserved after recoverable errors.

Chapter Summary

Interaction design for React and Vue forms is fundamentally the design of predictable state transitions. Controlled inputs place values in framework state, whereas uncontrolled inputs leave the DOM as the immediate source of truth. React generally expresses one-way data flow through props and callbacks; Vue’s v-model offers concise two-way binding while still requiring clear ownership.

Complex forms benefit from immutable updates, reducers or composables, stable identifiers, and explicit policies for conditional fields. Validation should be layered: browser constraints and client checks improve feedback, while server-side validation, authorization, and security controls remain authoritative. Good error feedback is specific, timely, associated with its control, and usable without sight or a mouse. File inputs require special treatment because browsers protect local paths, and uploads must be validated and secured on the server. Reusable abstractions should standardise semantics without concealing behaviour. The strongest professional implementations combine framework knowledge with HTML, accessibility, security, and user-centred design.

Key Terms

Controlled input: A form control whose current value is owned by framework state.

Uncontrolled input: A form control whose current value is primarily maintained by the DOM.

Two-way binding: A mechanism that synchronises a data value and a control’s user-visible value.

One-way data flow: A state model in which data moves in an explicit direction, commonly from parent to child.

Event handler: A function executed in response to an interaction event.

Touched field: A field with which the user has interacted, commonly by focusing and leaving it.

Dirty field: A field whose value differs from its initial value.

Constraint validation: Validation based on HTML attributes and input types.

Domain validation: Checking whether data satisfies application-specific rules.

Cross-field validation: Validation involving two or more related values.

Server-side validation: Authoritative validation performed by the backend.

Error summary: A consolidated list of form errors, often linking to controls.

File object: A browser representation of a user-selected local file.

Reducer: A function that calculates next state from current state and an action.

Composable: A reusable Vue function that encapsulates reactive logic.

Custom hook: A reusable React function that encapsulates stateful logic.

Progressive enhancement: Designing a usable baseline with HTML and progressively adding scripting.

CSRF: Cross-site request forgery, in which an unwanted request is induced from a user’s browser.

Review Questions

Short Answer Questions

  1. What is the principal difference between a controlled and an uncontrolled input?
  2. Why should a React text input normally be initialised with an empty string rather than undefined?
  3. What is the purpose of aria-describedby in a form?
  4. Why is client-side validation insufficient for security?
  5. Why should repeated form rows use stable keys rather than array indexes?

Long Answer Questions

  1. Compare React’s one-way data flow with Vue’s v-model binding. Discuss benefits and risks of each approach.
  2. Explain how validation timing affects usability and describe a suitable policy for blur, change, and submit events.
  3. Design a state model for a checkout form containing customer details, delivery options, products, and payment errors.
  4. Explain the security and usability considerations involved in file uploads.
  5. Describe how an accessible error-summary pattern should work after a failed form submission.

Analytical / Application Questions

  1. A form loses all entered values whenever the server returns an error. Analyse the likely state-management problem and propose a correction.
  2. A conditional address field is hidden when online delivery is selected, but its old value is still submitted. Explain why this occurs and design two valid solutions.
  3. A registration form validates every keystroke and displays “invalid email” after the first character. Evaluate this interaction and propose a more appropriate validation strategy.

Multiple Choice Questions with Answers

Question 1

Which statement best describes a controlled React input?

A. The browser permanently owns the value and React never reads it.

B. React state supplies the value and an event handler updates that state.

C. The input can be changed only by a server response.

D. The input must always be a file input.

Correct Answer: B. React state supplies the value and an event handler updates that state.

Question 2

Which React property represents the checked state of a controlled checkbox?

A. value

B. selected

C. checked

D. active

Correct Answer: C. checked

Question 3

What is the main purpose of preventing a form’s default submit action in a single-page application?

A. To disable keyboard navigation

B. To stop the browser’s default navigation while application code handles submission

C. To bypass server validation

D. To remove the form’s labels

Correct Answer: B. To stop the browser’s default navigation while application code handles submission

Question 4

Which statement about client-side validation is correct?

A. It replaces server-side authorization.

B. It guarantees that uploaded files are safe.

C. It improves feedback but cannot be treated as a security boundary.

D. It makes HTTPS unnecessary.

Correct Answer: C. It improves feedback but cannot be treated as a security boundary.

Question 5

Which attribute associates an input with its visible label in standard HTML?

A. The label’s for attribute matching the input’s id

B. The label’s name matching the input’s value

C. The input’s title matching the label’s class

D. The form’s action matching the label’s id

Correct Answer: A. The label’s for attribute matching the input’s id

Question 6

Why are stable keys important for repeated form rows?

A. They encrypt the row values.

B. They help the framework preserve the identity, focus, and state of each row.

C. They automatically validate every field.

D. They prevent all server errors.

Correct Answer: B. They help the framework preserve the identity, focus, and state of each row.

Question 7

What does Vue’s v-model commonly provide for a native text input?

A. A connection between a reactive value and the input’s value/input events

B. Automatic server-side authorization

C. Automatic file scanning

D. A replacement for HTML labels

Correct Answer: A. A connection between a reactive value and the input’s value/input events

Question 8

Why can a file input not normally be assigned an arbitrary local path by application code?

A. Because CSS prevents it

B. Because frameworks do not support strings

C. Because browsers restrict access to local files for security

D. Because files have no metadata

Correct Answer: C. Because browsers restrict access to local files for security

Question 9

Which feedback is most actionable?

A. Error

B. Wrong field

C. Invalid input

D. Enter a valid email address, such as [email protected].

Correct Answer: D. Enter a valid email address, such as [email protected].

Question 10

What should usually happen after a failed submission containing several errors?

A. All entered values should be erased.

B. The first invalid control may receive focus and errors should be programmatically associated with controls.

C. The submit button should be permanently removed.

D. Only a colour change should indicate the errors.

Correct Answer: B. The first invalid control may receive focus and errors should be programmatically associated with controls.

Frontend Frameworks (React.js/Vue.js)
Chapter 5 of 14