Muhi Logo Text
Work With MeAboutTestimonialsBlog

MUI TextField and Form Validation

Explore the details of validating Material UI (MUI) TextFields and Forms in React. Leveraging HTML's native 'required' validation and implementing custom error validations.

Last updated on January 17, 2024

react
mui
MUI Validation

Validating TextFields and Forms is an essential aspect of web application development. This tutorial navigates through various validation techniques, emphasizing the use of React and Material UI (MUI). We will explore both the HTML built-in ‘required’ validation and the creation of custom TextField error validations, providing a robust understanding of form validation strategies.

Here are the main topics that we will cover:

  1. Implementing MUI TextField Validation: This section will guide you through using HTML’s native ‘required’ and ‘pattern’ attributes.
  2. Creating Custom Error Validation for MUI TextFields: Add flexibility to your TextField validation by implementing custom error validation.
  3. MUI Form Validation Strategies: Learn to integrate multiple input fields into a cohesive MUI form validation process, ensuring overall data integrity and user-friendly interactions.

TextField Validation

Since we’re using MUI, the TextField component comes with handy props for applying styles and displaying error messages out of the box.

<TextField
  error
  label="Error"
  defaultValue="Name 123"
  helperText="Incorrect entry."
/>

The error prop will apply default styling with a red border and label, and the helperText prop will display the error message below the TextField.

Now, let’s see how to apply validation using both built-in HTML and custom error validation.

TextField Required Validation

Required is a built-in HTML attribute that can be used to validate an empty field. We can simply add the required attribute to the TextField component to use it.

<TextField required label="Required" />

We must create an onChange event handler to validate the user input and display the error message when the input field is empty.

Since required is a built-in HTML validation, we can use the validity.valid property to check whether the input field is empty.

import React, { useState } from "react";
import { TextField } from "@mui/material";

export default function TextFieldValidation() {
  const [name, setName] = useState("");
  const [nameError, setNameError] = useState(false);

  const handleNameChange = e => {
    setName(e.target.value);
    if (e.target.validity.valid) {
      setNameError(false);
    } else {
      setNameError(true);
    }
  };

  return (
    <TextField
      required
      label="Required"
      value={name}
      onChange={handleNameChange}
      error={nameError}
      helperText={nameError ? "Please enter your name" : ""}
    />
  );
}

In the example above, when typing and deleting the input field, the error message will show up. We didn’t add any logic to check if the TextField is empty because the required attribute will take care of it! We only need to show the error message if it’s invalid using the helperText prop.

TextField Pattern Validation

Pattern is another built-in HTML attribute that can be used for more complex validation. For example, we can enhance the previous validation only to accept letters and spaces.

import React, { useState } from "react";
import { TextField } from "@mui/material";

export default function TextFieldValidation() {
  const [name, setName] = useState("");
  const [nameError, setNameError] = useState(false);

  const handleNameChange = e => {
    setName(e.target.value);
    if (e.target.validity.valid) {
      setNameError(false);
    } else {
      setNameError(true);
    }
  };

  return (
    <TextField
      required
      label="Required"
      value={name}
      onChange={handleNameChange}
      error={nameError}
      helperText={
        nameError ? "Please enter your name (letters and spaces only)" : ""
      }
      inputProps={{
        pattern: "[A-Za-z ]+",
      }}
    />
  );
}

We should see the error message if we try to type in numbers in the example above.

Note that we added the pattern attribute to the inputProps because the TexField component is a wrapper that has the input element along with other elements. inputProps will pass the pattern attribute to the input element directly.

Custom Error Validation

The previous approach works great but doesn’t necessarily cover all the use cases, and regex can sometimes be tricky to read and maintain. Alternatively, we can add a validation logic in the event handle and display the appropriate error message. Let’s take an “Age” input field as an example. We want to validate that the user is at least 18 years old.

import React, { useState } from "react";
import { TextField } from "@mui/material";

export default function TextFieldValidation() {
  const [age, setAge] = useState("");
  const [ageError, setAgeError] = useState(false);

  const handleAgeChange = e => {
    setAge(e.target.value);
    if (e.target.value >= 18) {
      setAgeError(false);
    } else {
      setAgeError(true);
    }
  };

  return (
    <TextField
      label="Age"
      value={age}
      onChange={handleAgeChange}
      error={ageError}
      helperText={ageError ? "You must be at least 18 years old" : ""}
    />
  );
}

If we try to type in a number less than 18, we should see the error message “You must be at least 18 years old”.

Additionally, we can add different error messages if the age is invalid or negative.

import React, { useState } from "react";
import { TextField } from "@mui/material";

export default function TextFieldValidation() {
  const [age, setAge] = useState("");
  const [ageError, setAgeError] = useState(false);

  const handleAgeChange = e => {
    setAge(e.target.value);
    if (e.target.value >= 18) {
      setAgeError(false);
    } else if (e.target.value < 0) {
      setAgeError("Age must be a positive number");
    } else {
      setAgeError("You must be at least 18 years old");
    }
  };

  return (
    <TextField
      label="Age"
      value={age}
      onChange={handleAgeChange}
      error={ageError}
      helperText={ageError}
    />
  );
}

The new code should display the error message “Age must be a positive number” if the age is negative.

Form Validation

Now that we know how to validate a single TextField, let’s move on to validating multiple input fields in a form.

Similar to the previous section, we will cover built-in HTML and custom error validation for the entire form.

Form with Built-in HTML Validation

We can leverage the form onSubmit event handler for validation when using a form. It has a built-in function called checkValidity() that returns whether or not the input fields are valid.

Let’s create an example with the following requirements:

  • Create a form element with two input fields (Name and Email)
  • “Name” will only accept letters and spaces, and “Email” will only accept a valid email address
  • Validate the form on submit
  • Trigger alert with an error or success message
  • Add noValidate attribute to the Form to turn off the default HTML validation. Otherwise, the HTML tooltip validation will show up.
import React, { useState } from "react";
import { TextField, Box, Button } from "@mui/material";

export default function FormValidation() {
  const handleSubmit = e => {
    e.preventDefault();
    if (e.target.checkValidity()) {
      alert("Form is valid! Submitting the form...");
    } else {
      alert("Form is invalid! Please check the fields...");
    }
  };

  return (
    <Box component="form" onSubmit={handleSubmit} noValidate>
      <TextField
        required
        label="Name"
        inputProps={{
          pattern: "[A-Za-z ]+",
        }}
      />
      <TextField
        required
        label="Email"
        inputProps={{
          type: "email",
        }}
      />
      <Button variant="contained" color="primary" type="submit">
        Submit
      </Button>
    </Box>
  );
}

We can see how the handleSubmit function has the event object as an argument that allows us to use the checkValidity() method to check if the form is valid.

When trying the example above, we should see a valid or invalid alert message if we try to submit the form.

Note that we didn’t add a pattern attribute to the “Email” TextField because an input with type “email” already has a built-in validation specifically for email addresses!

Let’s update the example to include the onChange TextField validation.

import React, { useState } from "react";
import { TextField, Box, Button } from "@mui/material";

export default function FormValidation() {
  const [name, setName] = useState("");
  const [nameError, setNameError] = useState(false);
  const [email, setEmail] = useState("");
  const [emailError, setEmailError] = useState(false);

  const handleNameChange = e => {
    setName(e.target.value);
    if (e.target.validity.valid) {
      setNameError(false);
    } else {
      setNameError(true);
    }
  };

  const handleEmailChange = e => {
    setEmail(e.target.value);
    if (e.target.validity.valid) {
      setEmailError(false);
    } else {
      setEmailError(true);
    }
  };

  const handleSubmit = e => {
    e.preventDefault();
    if (e.target.checkValidity()) {
      alert("Form is valid! Submitting the form...");
    } else {
      alert("Form is invalid! Please check the fields...");
    }
  };
  return (
    <Box component="form" onSubmit={handleSubmit} noValidate>
      <TextField
        required
        label="Name"
        value={name}
        onChange={handleNameChange}
        error={nameError}
        inputProps={{
          pattern: "[A-Za-z ]+",
        }}
        helperText={
          nameError ? "Please enter your name (letters and spaces only)" : ""
        }
      />
      <TextField
        required
        label="Email"
        value={email}
        onChange={handleEmailChange}
        error={emailError}
        helperText={emailError ? "Please enter a valid email" : ""}
        inputProps={{
          type: "email",
        }}
      />
      <Button variant="contained" color="primary" type="submit">
        Submit
      </Button>
    </Box>
  );
}

Now we have both validation, onChange and onSubmit. One is for input field change, and the other is for form validation on submit.

Form with Custom Error Validation

As mentioned earlier, using the custom error validation approach gives us more flexibility and control over the validation logic but also requires more work.

Let’s take the previous example and add a custom error validation for the input change handlers.

Here are the requirements for the example:

  • “Name” will only accept letters and spaces with a length between 3 and 20 characters
  • “Email” will only accept a valid email address that contains a domain name and ’@’ symbol
const [name, setName] = useState("");
const [nameError, setNameError] = useState("");
const [email, setEmail] = useState("");
const [emailError, setEmailError] = useState(false);

const handleNameChange = e => {
  setName(e.target.value);
  if (e.target.value.length < 3) {
    setNameError("Name must be at least 3 characters long");
  } else if (e.target.value.length > 20) {
    setNameError("Name must be less than 20 characters long");
  } else if (!/^[a-zA-Z ]+$/.test(e.target.value)) {
    setNameError("Name must contain only letters and spaces");
  } else {
    setNameError(false);
  }
};

const handleEmailChange = e => {
  setEmail(e.target.value);
  if (!/^[a-zA-Z0-9._:$!%-]+@[a-zA-Z0-9.-]+.[a-zA-Z]$/.test(e.target.value)) {
    setEmailError("Invalid email address");
  } else {
    setEmailError(false);
  }
};

We can remove the pattern and required attributes for the elements because now we’re handling the validation logic in the event handler.

<Box component="form" onSubmit={handleSubmit} noValidate>
  <TextField
    label="Name"
    value={name}
    onChange={handleNameChange}
    error={nameError}
    helperText={nameError}
  />
  <TextField
    label="Email"
    value={email}
    onChange={handleEmailChange}
    error={emailError}
    helperText={emailError}
  />
  <Button variant="contained" color="primary" type="submit">
    Submit
  </Button>
</Box>

Finally, we need to update the handleSubmit event handler to check if the form is valid or not:

const handleSubmit = e => {
  e.preventDefault();
  if (nameError || emailError) {
    alert("Form is invalid! Please check the fields...");
  } else {
    alert("Form is valid! Submitting the form...");
  }
};

Here are all the pieces put together.

import React, { useState } from "react";
import { TextField, Box, Button } from "@mui/material";

export default function FormValidation() {
  const [name, setName] = useState("");
  const [nameError, setNameError] = useState("");
  const [email, setEmail] = useState("");
  const [emailError, setEmailError] = useState(false);

  const handleNameChange = e => {
    setName(e.target.value);
    if (e.target.value.length < 3) {
      setNameError("Name must be at least 3 characters long");
    } else if (e.target.value.length > 20) {
      setNameError("Name must be less than 20 characters long");
    } else if (!/^[a-zA-Z ]+$/.test(e.target.value)) {
      setNameError("Name must contain only letters and spaces");
    } else {
      setNameError(false);
    }
  };

  const handleEmailChange = e => {
    setEmail(e.target.value);
    if (!/^[a-zA-Z0-9._:$!%-]+@[a-zA-Z0-9.-]+.[a-zA-Z]$/.test(e.target.value)) {
      setEmailError("Invalid email address");
    } else {
      setEmailError(false);
    }
  };

  const handleSubmit = e => {
    e.preventDefault();
    if (nameError || emailError) {
      alert("Form is invalid! Please check the fields...");
    } else {
      alert("Form is valid! Submitting the form...");
    }
  };

  return (
    <Box component="form" onSubmit={handleSubmit} noValidate>
      <TextField
        label="Name"
        value={name}
        onChange={handleNameChange}
        error={nameError}
        helperText={nameError}
      />
      <TextField
        label="Email"
        value={email}
        onChange={handleEmailChange}
        error={emailError}
        helperText={emailError}
      />
      <Button variant="contained" color="primary" type="submit">
        Submit
      </Button>
    </Box>
  );
}

The code example should validate both text field change and form submission.

Code Refactoring and Optimization

The code we’ve written so far works great but could cause some maintainability and performance issues as we scale, especially when having a lot of input fields in the form. Let’s break down the code and see how to refactor and optimize it.

First of all, every time we trigger onChange, everything is re-rendered. That means if we have ten input fields, every time we type in one of them, all ten input fields will be re-rendered. To avoid that, we can create an isolated component for the TextField and only re-render that component when the value changes.

const ValidatedTextField = ({ label, validator, onChange }) => {
  const [value, setValue] = useState("");
  const [error, setError] = useState(false);

  const handleChange = e => {
    const newValue = e.target.value;
    const errorMessage = validator(newValue);
    setValue(newValue);
    setError(errorMessage);
    onChange(!errorMessage);
  };

  return (
    <TextField
      label={label}
      value={value}
      onChange={handleChange}
      error={!!error}
      helperText={error}
    />
  );
};

The component returns a MUI TextField, but since it’s generic, we must pass a few props to make it work.

  • label: The label of the TextField (in our example, it’s “Email” or “Name”)
  • validator: A function that takes the value of the input field and returns an error message if the value is invalid or false if the value is valid
  • onChange: A function that takes a boolean value to indicate whether the input field is valid. This is required for the Form validation later on.

Let’s create the two validators for the “Name” and “Email” input fields:

const nameValidator = value => {
  if (value.length < 3) return "Name must be at least 3 characters long";
  if (value.length > 20) return "Name must be less than 20 characters long";
  if (!/^[a-zA-Z ]+$/.test(value))
    return "Name must contain only letters and spaces";
  return false;
};

const emailValidator = value => {
  if (!/^[a-zA-Z0-9._:$!%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]+$/.test(value))
    return "Invalid email address";
  return false;
};

The function’s logic is the same as the previous example, but instead of setting the error message to the state, we’re returning it. Also, the two functions don’t necessarily need to be in a specific component and can be moved to a separate file if required.

Lastly, in the main FormValidation component, we can now use the new ValidatedTextField component instead of the TextField. Here are the changes we need to make to get it working with the new arrangement:

  • Remove all the state variables and event handlers as they are now handled in the ValidatedTextField component
  • Pass in the validators to the ValidatedTextField component
  • Create a new ref object to store the validation status of each input field. The object will act as a map where the key is the input field name, and the value is a boolean indicating whether the input field is valid. The reason we use ref instead of the state is that we don’t want to re-render the component every time the validation status changes
  • Use the onChange from the new ValidatedTextField component as a callback to update the validation status in the ref object
  • Update the handleSubmit event handler to check the validation status in the ref object by using Object.values and every methods
export default function FormValidation() {
  const formValid = useRef({ name: false, email: false });

  const handleSubmit = e => {
    e.preventDefault();
    if (Object.values(formValid.current).every(isValid => isValid)) {
      alert("Form is valid! Submitting the form...");
    } else {
      alert("Form is invalid! Please check the fields...");
    }
  };

  return (
    <Box component="form" onSubmit={handleSubmit} noValidate>
      <ValidatedTextField
        label="Name"
        validator={nameValidator}
        onChange={isValid => (formValid.current.name = isValid)}
      />
      <ValidatedTextField
        label="Email"
        validator={emailValidator}
        onChange={isValid => (formValid.current.email = isValid)}
      />
      <Button type="submit" variant="contained">
        Submit
      </Button>
    </Box>
  );
}

Now, let’s put everything together and see what the final code looks like.

import React, { useState, useRef } from "react";
import { TextField, Box, Button } from "@mui/material";

// ValidatedTextField.js
const ValidatedTextField = ({ label, validator, onChange }) => {
  const [value, setValue] = useState("");
  const [error, setError] = useState(false);

  const handleChange = e => {
    const newValue = e.target.value;
    const errorMessage = validator(newValue);
    setValue(newValue);
    setError(errorMessage);
    onChange(!errorMessage);
  };

  return (
    <TextField
      label={label}
      value={value}
      onChange={handleChange}
      error={!!error}
      helperText={error}
    />
  );
};

// validators.js
const nameValidator = value => {
  if (value.length < 3) return "Name must be at least 3 characters long";
  if (value.length > 20) return "Name must be less than 20 characters long";
  if (!/^[a-zA-Z ]+$/.test(value))
    return "Name must contain only letters and spaces";
  return false;
};

const emailValidator = value => {
  if (!/^[a-zA-Z0-9._:$!%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]+$/.test(value))
    return "Invalid email address";
  return false;
};

// FormValidation.js
export default function FormValidation() {
  const formValid = useRef({ name: false, email: false });

  const handleSubmit = e => {
    e.preventDefault();
    if (Object.values(formValid.current).every(isValid => isValid)) {
      alert("Form is valid! Submitting the form...");
    } else {
      alert("Form is invalid! Please check the fields...");
    }
  };

  return (
    <Box component="form" onSubmit={handleSubmit} noValidate>
      <ValidatedTextField
        label="Name"
        validator={nameValidator}
        onChange={isValid => (formValid.current.name = isValid)}
      />
      <ValidatedTextField
        label="Email"
        validator={emailValidator}
        onChange={isValid => (formValid.current.email = isValid)}
      />
      <Button type="submit" variant="contained">
        Submit
      </Button>
    </Box>
  );
}

The outcome is the same as the previous example. Still, now we have a more generic and reusable input field component that can be used without repeatedly writing the same component. As indicated in the comments, we can split the code into separate files for better organization.

Styling and Colors

To customize the colors and other styles of the TextField error state, we can use the Mui-error class that is added to the TextField when the error prop is true.

In the following example, we can use the error class to style the helperText and the input field border and label colors.

...
<TextField
  ...
  sx={{
    "& .MuiInputLabel-root.Mui-error": {
      color: "#ff8804",
    },
    "& .MuiOutlinedInput-root.Mui-error .MuiOutlinedInput-notchedOutline": {
      border: "3px solid #ff8804",
    },
    "& .MuiFormHelperText-root.Mui-error": {
      color: "#ff8804",
    },
  }}
/>

Conclusion

In this tutorial, we learned how to validate MUI TextField and Form using built-in HTML and custom error validation with code refactoring and optimization. Although plenty of useful libraries can help us with the validation, it’s always good to understand the underlying principles and build our own validation logic.

Bye for now 👋

If you enjoyed this post, I regularly share similar content on Twitter. Follow me @muhimasri to stay up to date, or feel free to message me on Twitter if you have any questions or comments. I'm always open to discussing the topics I write about!

Recommended Reading

Learn how to create, customize & style vertical tabs in React & MUI with icons, labels, and custom styles.

react
mui

Discussion

Upskill Your Frontend Development Techniques 🌟

Subscribe to stay up-to-date and receive quality front-end development tutorials straight to your inbox!

No spam, sales, or ads. Unsubscribe anytime you wish.

© 2024, Muhi Masri