ProgramGuru

Form Validation

Form Validation

A teacher enters a maths mark outside the school’s permitted 0–100 range. The interface should explain the problem instead of silently saving an impossible result.

Validation checks whether entered data meets the task’s rules. Client-side validation helps the teacher correct a draft; authoritative server validation is still required when data is sent to a real school system.

  1. Understand

    Preserve the teacher’s raw input until checking it.

  2. Build

    Display a specific accessible validation message.

  3. Check

    Separate helpful browser checks from trusted server rules.

Draft, rule, useful feedback

  1. Draft

    The field contains the teacher’s string.

  2. Check

    Require a whole-number mark from 0 to 100.

  3. Feedback

    Show a reason or confirm the accepted local mark.

Read this relationship alongside the complete code. This diagram explains the flow; it does not execute React.

Define the rule before writing the check

This school accepts whole-number marks from 0 through 100. An empty field is missing data rather than zero, even though Number of an empty string would produce zero. Check the trimmed text first, then convert it and check Number.isInteger plus the range. The order preserves the difference between missing and valid zero.

An error should say what needs correcting. Mark must be a whole number from 0 to 100 is more useful than Invalid input. Associate the error paragraph with the input through aria-describedby and mark the field aria-invalid when an error is present. This makes feedback available beyond its visual position.

Check and explain a maths mark

This form uses noValidate to demonstrate the application’s complete rule and feedback. onSubmit handles both a button press and keyboard submission. The teacher can keep editing the raw mark string, while the handler only accepts it after the explicit checks. Saving here means updating a local display.

React · JavaScript with JSX

import { useState } from 'react';
export default function App() {
  const [mark, setMark] = useState('');
  const [error, setError] = useState('');
  const [saved, setSaved] = useState(null);
  function submit(event) {
    event.preventDefault();
    const value = Number(mark);
    if (!mark.trim() || !Number.isInteger(value) || value < 0 || value > 100) {
      setError('Mark must be a whole number from 0 to 100.');
      return;
    }
    setError(''); setSaved(value);
  }
  return <main><h1>Maths mark</h1><form noValidate onSubmit={submit}>
    <label>Mark <input value={mark} onChange={event => setMark(event.target.value)} aria-describedby="mark-error" aria-invalid={Boolean(error)} /></label>
    <p id="mark-error" role="alert">{error}</p><button>Save mark</button>
  </form><p>Saved: {saved === null ? 'none' : saved}</p></main>;
}

Expected screen and behavior

Initially no mark is saved. Submitting an empty or out-of-range mark shows the rule. Entering 85 and saving shows Saved: 85.

Trace the invalid and valid paths

React · Check and explain a maths mark

import { useState } from 'react';
export default function App() {
  const [mark, setMark] = useState('');
  const [error, setError] = useState('');
  const [saved, setSaved] = useState(null);
  function submit(event) {
    event.preventDefault();
    const value = Number(mark);
    if (!mark.trim() || !Number.isInteger(value) || value < 0 || value > 100) {
      setError('Mark must be a whole number from 0 to 100.');
      return;
    }
    setError(''); setSaved(value);
  }
  return <main><h1>Maths mark</h1><form noValidate onSubmit={submit}>
    <label>Mark <input value={mark} onChange={event => setMark(event.target.value)} aria-describedby="mark-error" aria-invalid={Boolean(error)} /></label>
    <p id="mark-error" role="alert">{error}</p><button>Save mark</button>
  </form><p>Saved: {saved === null ? 'none' : saved}</p></main>;
}

Submitting the empty field prevents the browser’s default navigation and checks the current draft. The empty-string guard rejects it, so the handler stores an error and returns before changing saved. The next render displays that error and marks the field invalid. The teacher has not lost the draft.

After entering 85, another submission converts the text to the number 85. All checks pass. The handler clears the error and updates saved, so React describes an accepted mark. The saved display stays independent of later draft edits until another successful submission, matching a teacher’s expectation of an explicit Save action.

Decide when feedback helps

Submission-time validation avoids showing errors before the teacher has tried to complete a field. Some forms also validate after blur or while editing a previously invalid field. Choose timing that helps correction without declaring a normal unfinished draft wrong after every keystroke.

HTML constraints such as required, min, max and step can provide useful native validation. Custom school rules may still need application checks. Do not combine inconsistent native and custom messages by accident. For real saves, handle a server rejection as another useful error rather than assuming the browser’s acceptance guarantees success.

Try the school task

React · starting example

import { useState } from 'react';
export default function App() {
  const [mark, setMark] = useState('');
  const [error, setError] = useState('');
  const [saved, setSaved] = useState(null);
  function submit(event) {
    event.preventDefault();
    const value = Number(mark);
    if (!mark.trim() || !Number.isInteger(value) || value < 0 || value > 100) {
      setError('Mark must be a whole number from 0 to 100.');
      return;
    }
    setError(''); setSaved(value);
  }
  return <main><h1>Maths mark</h1><form noValidate onSubmit={submit}>
    <label>Mark <input value={mark} onChange={event => setMark(event.target.value)} aria-describedby="mark-error" aria-invalid={Boolean(error)} /></label>
    <p id="mark-error" role="alert">{error}</p><button>Save mark</button>
  </form><p>Saved: {saved === null ? 'none' : saved}</p></main>;
}

Validate a student name as non-empty after trimming. An all-space draft must show Enter a student name and keep Saved name: none. A valid draft should display the trimmed name. Make the field label and error relationship explicit in the complete answer.

Worked answer

The answer trims only when checking and saving, so typing remains predictable. noValidate lets the application demonstrate its own message. A blank submission sets the error; a valid one clears it and saves the trimmed string. The example intentionally stays local to focus on the validation decision.

React · JavaScript with JSX

import { useState } from 'react';
export default function App() {
  const [name, setName] = useState('');
  const [error, setError] = useState('');
  const [saved, setSaved] = useState('none');
  function submit(event) {
    event.preventDefault();
    const cleaned = name.trim();
    if (!cleaned) { setError('Enter a student name.'); return; }
    setError(''); setSaved(cleaned);
  }
  return <main><h1>Student name check</h1><form noValidate onSubmit={submit}>
    <label>Student name <input value={name} onChange={event => setName(event.target.value)} aria-describedby="name-error" aria-invalid={Boolean(error)} /></label>
    <p id="name-error" role="alert">{error}</p><button>Save name</button>
  </form><p>Saved name: {saved}</p></main>;
}

Expected screen and behavior

A blank submission displays Enter a student name. Saving a valid Ravi draft shows Saved name: Ravi.

Mistakes and boundaries

Never rely on client validation to enforce trusted school records. A caller can bypass the form and send another request. The server must validate data, permissions and business rules itself. Helpful client feedback and authoritative server checks serve different purposes.

Do not erase a rejected draft or show success before the intended save is accepted. Keep errors specific to the field when possible and explain request failures separately. A value can satisfy a numeric range and still refer to the wrong student, so validation also depends on the surrounding task.

Check your reasoning

React · Check and explain a maths mark

import { useState } from 'react';
export default function App() {
  const [mark, setMark] = useState('');
  const [error, setError] = useState('');
  const [saved, setSaved] = useState(null);
  function submit(event) {
    event.preventDefault();
    const value = Number(mark);
    if (!mark.trim() || !Number.isInteger(value) || value < 0 || value > 100) {
      setError('Mark must be a whole number from 0 to 100.');
      return;
    }
    setError(''); setSaved(value);
  }
  return <main><h1>Maths mark</h1><form noValidate onSubmit={submit}>
    <label>Mark <input value={mark} onChange={event => setMark(event.target.value)} aria-describedby="mark-error" aria-invalid={Boolean(error)} /></label>
    <p id="mark-error" role="alert">{error}</p><button>Save mark</button>
  </form><p>Saved: {saved === null ? 'none' : saved}</p></main>;
}

Choose the answer that follows the data and the interaction, then read the explanation for each alternative.

Why does the mark handler test !mark.trim() before accepting Number(mark)?
Choose one answer. You can try again.

Answer and explanation

Converting an empty string yields zero, so conversion alone could accept missing data.

Correct. The empty-text check keeps a blank draft distinct from the valid mark 0.

Carry the idea forward

Define the school rule, preserve the editable draft and give a clear reason when it fails. Continue with submitting forms to model request progress and distinguish local acceptance from a server save.

Read the primary reference: React: input constraints and controlled fields.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Submitting Forms →