# Form Validation

Course: [React](https://www.devtutorials.ai/learn/react) · Section: Forms
Source: https://www.devtutorials.ai/learn/react/forms/form-validation

> Required fields, email and password checks, custom rules, validating on submit or in real time, and writing clear error messages.

## Introduction to Form Validation

**Form validation** means checking what the user typed before you use it. For example: is the email box empty? Is the password long enough?

If something is wrong, you show a clear **error message** and do not send the form. If everything is right, you send it.

### Real-World Example

A bookshop sign-up form checks the data before it sends it. The email must contain `@`, the password must have at least 8 characters, and both passwords must match.

Each error appears under its input, for example "Password is too short". The **Sign up** button sends the form only when the `errors` object is empty.

<figure className="react-fig">
<svg viewBox="0 0 520 180" role="img" aria-labelledby="fig-valid-title" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-valid-title">Validation flow: the user submits, the form is checked, errors go back to the user, a valid form is sent</title>
  <g style={{ fontFamily: "var(--sans)", fontSize: 13, fontWeight: 600 }}>
    <rect x="10" y="66" width="120" height="44" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)" }} />
    <text x="70" y="93" textAnchor="middle" style={{ fill: "var(--accent)" }}>User submits</text>
    <rect x="200" y="66" width="120" height="44" rx="22" style={{ fill: "var(--panel)", stroke: "var(--ink2)" }} />
    <text x="260" y="93" textAnchor="middle" style={{ fill: "var(--ink)" }}>Check fields</text>
    <rect x="390" y="14" width="120" height="44" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)" }} />
    <text x="450" y="41" textAnchor="middle" style={{ fill: "var(--ok)" }}>Send the form</text>
    <rect x="390" y="118" width="120" height="44" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)" }} />
    <text x="450" y="145" textAnchor="middle" style={{ fill: "var(--bad)" }}>Show errors</text>
  </g>
  <g style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }}>
    <path d="M130 88 L196 88" />
    <path d="M320 80 L386 40" />
    <path d="M320 96 L386 136" />
    <path d="M390 150 Q70 190 70 114" style={{ strokeDasharray: "5 4" }} />
  </g>
  <g style={{ fontFamily: "var(--sans)", fontSize: 11, fill: "var(--muted)" }}>
    <text x="350" y="50">all OK</text>
    <text x="350" y="132">problems</text>
    <text x="200" y="176">user fixes and tries again</text>
  </g>
  <circle r="6" style={{ fill: "var(--accent)" }}>
    <animateMotion dur="4s" repeatCount="indefinite" path="M130 88 L196 88 L320 96 L386 136 M390 150 Q70 190 70 114 M130 88 L196 88 L320 80 L386 40" />
  </circle>
</svg>
<figcaption>The form is checked first. Problems go back to the user. Only a correct form is sent.</figcaption>
</figure>

Validation in the browser helps the user. A real app must also check the data again on the server, because browser code can be changed.

## Required Fields

A **required field** is a box that must not be empty. To check it, look at the value after removing spaces at the start and end with `trim()`.

```tsx
if (name.trim() === "") {
  // the box is empty, or has only spaces
}
```

`trim()` matters. Without it, a box with only spaces would pass the check.

      <input placeholder="Your name" value={name} onChange={(e) => setName(e.target.value)} />
      {error && <p className="error">{error}</p>}
      <button type="submit">Join</button>
      {done && <p className="ok">Welcome, {name}!</p>}
    </form>
  );
}
`,
    "App.css": `.form { display: grid; gap: 6px; max-width: 260px; }
.error { color: #dc2626; margin: 0; font-size: 14px; }
.ok { color: #15803d; margin: 0; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Press **Join** with an empty box. The error shows. Type only spaces and press it again. The error still shows, because of `trim()`. Type a name, and the welcome message appears.

Look at `return;` inside the `if`. It stops the function, so the form is not sent when there is an error.

### The required Attribute

HTML also has a `required` attribute: `<input required />`. The browser then shows its own small popup when the box is empty.

It is quick, but you cannot change how the popup looks, and it only checks for empty boxes. Most React apps write their own checks, as in this lesson, so they can show their own messages.

## Input Validation

A box can be filled in and still be wrong. **Input validation** checks that the value has the right **form**, such as a real email or a strong enough password.

### Email Validation

A simple email check looks for some text, then `@`, then some text, then a dot, then some more text. You can check this with a **regular expression**: a pattern that text must match.

```tsx
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

emailPattern.test("asha@example.com"); // true
emailPattern.test("asha@example");     // false (no dot after @)
emailPattern.test("asha example.com"); // false (no @)
```

You do not need to understand every symbol in the pattern now. Read it as: "some letters, an `@`, some letters, a dot, some letters, and no spaces".

`.test(text)` gives back `true` if the text matches the pattern, and `false` if not.

A pattern only checks the shape of an email. It cannot tell if the email really exists. To be sure, apps send a confirmation email.

### Password Validation

Password rules are just `if` checks. Common rules are:

- At least 8 characters: `password.length >= 8`
- At least one number: `/[0-9]/.test(password)`
- At least one capital letter: `/[A-Z]/.test(password)`

      <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
      <p className={emailError ? "error" : "ok"}>{emailError || "Email looks good."}</p>

      <input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <p className={passwordError ? "error" : "ok"}>{passwordError || "Strong password."}</p>
    </div>
  );
}
`,
    "App.css": `.form { display: grid; gap: 4px; max-width: 320px; }
.form p { margin: 0 0 8px; font-size: 13px; }
.error { color: #dc2626; }
.ok { color: #15803d; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Type slowly in each box and watch the message change. For the password, try `book`, then `bookshop`, then `bookshop1`, then `Bookshop1`. Each step fixes one rule.

Each check function gives back an **error message**, or an empty text `""` when there is no error. This makes the checks easy to use anywhere.

### Custom Validation Logic

**Custom validation** is any rule your app needs, written as your own function. A common example is checking that two boxes match, such as "Password" and "Confirm password".

      <input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <input type="password" placeholder="Confirm password" value={confirm} onChange={(e) => setConfirm(e.target.value)} />
      <p className={confirmError ? "error" : "ok"}>{confirmError || "Passwords match."}</p>

      <input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
      <p className={dateError ? "error" : "ok"}>{dateError || "Delivery date is fine."}</p>
    </div>
  );
}
`,
    "App.css": `.form { display: grid; gap: 4px; max-width: 280px; }
.form p { margin: 0 0 8px; font-size: 13px; }
.error { color: #dc2626; }
.ok { color: #15803d; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Type the same password in both boxes, then change one letter. Then pick today's date, and then a later date. Each rule is its own small function.

Other examples of custom rules:

- The number of copies must be between 1 and 10.
- The delivery date must be in the future.
- A username must not already be taken.

Each one is a small function that takes the value and gives back an error message or `""`.

## Validation Strategies

There are two common times to check a form: when the user **submits**, or **while the user types**.

### Validation on Submit

The form is checked only when the user presses the submit button. All errors appear at once.

- **Good:** The user is not shown errors while still typing.
- **Not so good:** The user learns about every problem only at the end.

### Real-Time Validation

The form is checked on **every change**, so the message updates as the user types. The email and password lab above works this way.

- **Good:** The user sees problems at once and can fix them quickly.
- **Not so good:** Errors show before the user has finished typing, which can feel rude.

### A Common Mix

Many apps use a mix: show a field's error only **after the user has left that box**, or after they press submit. You track which boxes were left with a `touched` state, using the `onBlur` event. `onBlur` runs when the user leaves a box.

      <input
        placeholder="Email"
        value={values.email}
        onChange={(e) => setValues({ ...values, email: e.target.value })}
        onBlur={() => setTouched({ ...touched, email: true })}
      />
      {touched.email && errors.email && <p className="error">{errors.email}</p>}

      <input
        type="password"
        placeholder="Password"
        value={values.password}
        onChange={(e) => setValues({ ...values, password: e.target.value })}
        onBlur={() => setTouched({ ...touched, password: true })}
      />
      {touched.password && errors.password && <p className="error">{errors.password}</p>}

      <button type="submit">Create account</button>
      {sent && <p className="ok">Account created!</p>}
    </form>
  );
}
`,
    "App.css": `.form { display: grid; gap: 6px; max-width: 280px; }
.form p { margin: 0; font-size: 13px; }
.error { color: #dc2626; }
.ok { color: #15803d; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Click in the email box, type one letter, then click somewhere else. Only now does the email error appear. The password error stays hidden until you leave that box, or press **Create account**.

Look at this line:

```tsx
{touched.email && errors.email && <p className="error">{errors.email}</p>}
```

It shows the error only when **both** are true: the user has left the box, **and** there is an error.

`Partial<Values>` means "an object that may have some of the keys of `Values`". So `errors` can have an `email` key, a `password` key, both, or none.

Check on submit for short forms. For longer forms, show each error after the user leaves the box.

## Error Messages

A good error message helps the user fix the problem quickly. Follow these rules:

- **Say what to do**, not only what is wrong. "Enter an email like asha@example.com" is better than "Invalid input".
- **Show it next to the box** it is about, not only at the top of the form.
- **Use a clear look**, such as red text, and a red border on the box.
- **Use simple, polite words.** Do not blame the user.
- **Remove it** as soon as the problem is fixed.

<div className="tscroll">

| Not helpful | Helpful |
|---|---|
| Error | Please enter your name. |
| Invalid email | Enter an email like asha@example.com. |
| Password wrong | Use at least 8 characters, with one number. |
| Bad input | Copies must be between 1 and 10. |

</div>

      <label htmlFor="copies">Copies of Atomic Habits</label>
      <input
        id="copies"
        className={error ? "has-error" : ""}
        value={copies}
        onChange={(e) => setCopies(e.target.value)}
        aria-invalid={error !== ""}
      />
      {error ? <p className="error">{error}</p> : <p className="ok">Total: ₹{amount * 499}</p>}
    </div>
  );
}
`,
    "App.css": `.field { display: grid; gap: 4px; max-width: 260px; }
.field input { padding: 6px 8px; border: 1px solid #cbd5e1; border-radius: 6px; }
.field input.has-error { border-color: #dc2626; background: #fef2f2; }
.field p { margin: 0; font-size: 13px; }
.error { color: #dc2626; }
.ok { color: #15803d; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Try `0`, `2.5`, `15`, an empty box, and then `3`. Each wrong value gives its own message, and the box turns red. With `3`, the red goes away and the total appears.

`aria-invalid` tells screen readers that the box has a problem, so users who cannot see the red colour also know.

## Common mistakes

<div className="tscroll">

| Mistake | What you see | Fix |
|---|---|---|
| Checking `name === ""` without `trim()` | A box with only spaces passes | Use `name.trim() === ""` |
| Forgetting `return` after finding an error on submit | The form is sent even with errors | `return` right after setting the error |
| Showing every error before the user types | The form is red as soon as it opens | Show errors after submit, or after the user leaves a box |
| Not clearing the error after a fix | The old error stays on screen | Work the error out from the current values every render |
| Vague messages like "Invalid" | The user does not know what to fix | Say exactly what to do |
| Error far away from its box | The user cannot tell which box is wrong | Put each message right under its box |
| Only checking in the browser | Bad data can still reach the server | Check again on the server |

</div>

## Check Yourself

## Exercises

## What to Remember

- Validation checks the values before the form is sent. Use `trim()` for required fields.
- Write each rule as a small function that gives back an error message, or `""` when there is no error.
- Show clear, helpful messages next to each box: after submit, while typing, or after the user leaves the box.

Next: Understanding Hooks
