In this article, you will learn how to validate React.js forms without relying on external libraries. Good form validation can make the difference between a positive user experience and a negative one.

Therefore, we will not only handle the validation process, but I will also provide tips that you can use to enhance validation in your forms.

More practice

How to Validate React.js Forms Without External Libraries

Must-Know Tips for Effective Form Validation

  1. Choose the Appropriate HTML Input Types – Ensure you’re using the appropriate input types (e.g., email, number, password) to take advantage of built-in browser validation.
  2. Leverage HTML’s Built-in Validation Properties – Take advantage of HTML’s native validation attributes, such as required, minlength, maxlength, and pattern, to enforce basic rules without needing extra JavaScript or libraries.
  3. Define Custom Tooltips for Built-in Validators – Customize tooltip messages to guide users when validation errors occur. This can be achieved by using the title attribute on the input element.
  4. Provide Specific Validation Messages – Use clear and informative messages to guide users through correcting input errors.
  5. Pattern Matching with Regex – For custom formats, apply regular expressions to validate complex patterns, like phone numbers or postal codes.
  6. Highlight Invalid Inputs – Make it easy for users to see what needs attention by visually highlighting fields with invalid input.
  7. Hide Error Messages When Input is Valid – Ensure error messages disappear once the input meets validation requirements.
  8. Include Validation on the Server Side – Enhance security and reliability by validating form data on the server, even after client-side validation.

Run and Test the Validation

To test the project on your computer, simply follow the instructions below:

  • To get started, download or clone the project from its GitHub repository at https://github.com/wpcodevo/form-validation.
  • Navigate to the validate-reactjs-form-no-library directory and open the source code in your preferred code editor or IDE.
  • Open the integrated terminal in your IDE and run the command pnpm install to install the necessary dependencies.
  • After the dependencies have been installed, start the Vite development server by executing the command pnpm dev --port 3000.
  • Finally, open your browser and access the application to test the validation logic.

Overview of the Form Validation

We will implement form validation in React.js without relying on external validation libraries. This form will consist of multiple fields, each with its specific validation error message that will be displayed when invalid data is entered.

Below is an overview of the fields and their corresponding validation errors.

  • Username – Username must be at least 3 characters long and can contain letters, numbers, and underscores.
  • Email – Please enter a valid email address.
  • Date Of Birth – Please enter a valid date.
  • Password – Password must be at least 8 characters long, and contain at least one uppercase letter, one lowercase letter, one number, and one special character.
  • Confirm Password – Passwords must match.

Validation errors for each input will only be displayed when the user unfocuses the field, provided the entered data does not match the required format. Below is an example illustrating the validation errors for all the input fields.

validation errors shown on the form built with React.js and no external libraries involved

Fields that meet the required format will not display validation errors, as illustrated in the image below.

some fields are invalid while others are valid

When the data entered into the input fields is valid, there won’t be any validation errors, as shown in the image below.

create the form and style it with css

Creating the Form Input React Component

Let’s begin by creating a reusable input component. To make it more versatile, we’ll use React.forwardRef to forward its ref to the underlying input element. This will allow parent components to directly access the input DOM element.

components/form-input.tsx


import React, { useState } from 'react';

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {
  error?: string;
  label?: string;
}

const FormInput = React.forwardRef<HTMLInputElement, InputProps>(
  ({ type = 'text', name, label, error, className, ...props }, ref) => {
    const [focused, setFocus] = useState(false);
    const handleFocus = () => {
      setFocus(true);
    };
    return (
      <div className={`form-input ${className}`}>
        {label && <label htmlFor={name}>{label}</label>}
        <input
          ref={ref}
          type={type}
          name={name}
          className={`input ${error ? 'input-error' : ''} ${
            focused ? 'input-focused' : ''
          }`}
          onFocus={() => name === 'confirmPassword' && setFocus(true)}
          onBlur={handleFocus}
          {...props}
        />
        {error && <span className='error-message'>{error}</span>}
      </div>
    );
  }
);

FormInput.displayName = 'FormInput';

export { FormInput };

We defined a focus state and created a function called handleFocus to set this state to true. Then, we assigned handleFocus to the onBlur attribute of the input element.

In React, the onBlur event is triggered when an input field loses focus, meaning the user clicks or tabs out of it. Here, onBlur helps manage the focus state, which is used to show or hide error messages accordingly.

Creating the Form in React

Now that we’ve defined the input component, let’s use it to render the various input elements in the form. To reduce redundancy and improve code readability, we’ll define each input’s attributes in an array of objects, then map through them to render each input using our reusable component. Below is an example demonstrating this approach.

src/App.tsx


import './App.css';
import { FormInput } from '../components/form-input';

type FormValues = {
  username: string;
  email: string;
  dateOfBirth: string;
  password: string;
  confirmPassword: string;
};

type InputFieldData = {
  id: number;
  type: string;
  name: keyof FormValues;
  placeholder: string;
  label: string;
};

const inputFieldData: InputFieldData[] = [
  {
    id: 1,
    type: 'text',
    name: 'username',
    placeholder: 'Username',
    label: 'Username',
  },
  {
    id: 2,
    type: 'email',
    name: 'email',
    placeholder: 'Email',
    label: 'Email',
  },
  {
    id: 3,
    type: 'date',
    name: 'dateOfBirth',
    placeholder: 'Date Of Birth',
    label: 'Date Of Birth',
  },
  {
    id: 4,
    type: 'password',
    name: 'password',
    placeholder: 'Password',
    label: 'Password',
  },
  {
    id: 5,
    type: 'password',
    name: 'confirmPassword',
    placeholder: 'Confirm Password',
    label: 'Confirm Password',
  },
];

function App() {
  return (
    <div className='wrapper'>
      <form>
        {inputFieldData.map(({ id, name, ...props }) => {
          return <FormInput key={id} name={name} {...props} />;
        })}
        <button type='submit'>Register Account</button>
      </form>
    </div>
  );
}

export default App;

Styling the Form with CSS

Before diving into the validation logic, let’s start by styling the form and adding a background image to enhance its appearance. If you’re following along, add the following CSS to the src/App.css file.

src/App.css


* {
  box-sizing: border-box;
}

:root {
  --primary-color: #007bff;
  --primary-color-hover: #0056b3;
  --border-color: #ccc;
  --error-color: #ff0000;
  --font-family: Arial, sans-serif;
}

body {
  margin: 0;
  font-family: var(--font-family);
  background-image: url('bg.jpg');
  background-size: cover;
  background-position: center;
  color: #333;
}

.wrapper {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  max-width: 500px;
  margin: 0 auto;
}

form {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 20px;
  background-color: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}

.form-input {
  display: flex;
  flex-direction: column;
  margin-bottom: 15px;
}

label {
  margin-bottom: 5px;
  font-weight: bold;
}

input {
  padding: 10px;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  font-size: 16px;
  transition: border-color 0.3s ease;
}

input:focus {
  border-color: var(--primary-color);
  outline: none;
}

.error-message {
  color: var(--error-color);
  font-size: 12px;
  margin-top: 10px;
  display: none;
}

input:invalid.input-focused {
  border-color: var(--error-color);
}

input:invalid.input-focused ~ span {
  display: block;
}

button {
  padding: 10px;
  background-color: var(--primary-color);
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
  transition: background-color 0.3s ease;
}

button:hover {
  background-color: var(--primary-color-hover);
}

button:focus {
  outline: 2px solid var(--primary-color);
}

@media (max-width: 600px) {
  .wrapper {
    max-width: 90%;
  }
}

Looking through the CSS, you’ll notice that validation styling is applied directly to the input field. When an input has an invalid value, it checks for the presence of the input-focused class. If this class is present, the input’s border colour changes to red, and the error message becomes visible.

Writing the Validation Logic

With the styling for the input elements complete, we can now proceed to implement the validation logic. We’ll leverage HTML5’s pattern and required attributes to handle validation. In the input data object, add fields for errorMessage, required, and pattern, each with appropriate values. To make this easier, simply copy the code below and replace it with the existing code:

src/App.tsx


import './App.css';
import { FormInput } from '../components/form-input';
import React, { useState } from 'react';

type FormValues = {
  username: string;
  email: string;
  dateOfBirth: string;
  password: string;
  confirmPassword: string;
};

type InputFieldData = {
  id: number;
  type: string;
  name: keyof FormValues;
  placeholder: string;
  label: string;
  error: string;
  pattern?: string;
  required: boolean;
};

const inputFieldData: InputFieldData[] = [
  {
    id: 1,
    type: 'text',
    name: 'username',
    placeholder: 'Username',
    label: 'Username',
    error:
      'Username must be at least 3 characters long and can contain letters, numbers, and underscores.',
    pattern: '^[a-zA-Z0-9_]{3,}$',
    required: true,
  },
  {
    id: 2,
    type: 'email',
    name: 'email',
    placeholder: 'Email',
    label: 'Email',
    error: 'Please enter a valid email address.',
    pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
    required: true,
  },
  {
    id: 3,
    type: 'date',
    name: 'dateOfBirth',
    placeholder: 'Date Of Birth',
    label: 'Date Of Birth',
    error: 'Please enter a valid date.',
    pattern: '^\\d{4}-\\d{2}-\\d{2}$',
    required: true,
  },
  {
    id: 4,
    type: 'password',
    name: 'password',
    placeholder: 'Password',
    label: 'Password',
    error:
      'Password must be at least 8 characters long, and contain at least one uppercase letter, one lowercase letter, one number, and one special character.',
    pattern: '^(?=.*[A-Z])(?=.*[a-z])(?=.*[\\d\\W])[A-Za-z\\d\\W]{8,}$',
    required: true,
  },
  {
    id: 5,
    type: 'password',
    name: 'confirmPassword',
    placeholder: 'Confirm Password',
    label: 'Confirm Password',
    error: 'Passwords must match.',
    required: true,
  },
];

function App() {
  const [values, setValues] = useState<FormValues>({
    username: '',
    email: '',
    dateOfBirth: '',
    password: '',
    confirmPassword: '',
  });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setValues((prevValues) => ({
      ...prevValues,
      [name]: value,
    }));
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    console.log('Form submitted:', values);
  };

  return (
    <div className='wrapper'>
      <form onSubmit={handleSubmit}>
        {inputFieldData.map(({ id, name, ...props }) => {
          const dynamicPattern =
            name === 'confirmPassword' ? `^${values.password}$` : props.pattern;
          return (
            <FormInput
              key={id}
              name={name}
              value={values[name]}
              onChange={handleChange}
              pattern={dynamicPattern}
              {...props}
            />
          );
        })}
        <button type='submit'>Register Account</button>
      </form>
    </div>
  );
}

export default App;

To ensure the password-matching logic functions correctly, we dynamically set the pattern attribute of the confirm password input to match the value of the password input field.

Conclusion

As we wrap up this comprehensive guide, you’ve learned how to implement form validation in React.js using the built-in HTML5 pattern and required attributes, which means there’s no need for any advanced validation libraries. However, if you wish to handle more complex client-side validation logic, you will need to incorporate a validation library.

I hope you found this article both informative and enjoyable. If you have any questions or feedback, please don’t hesitate to leave a comment below. Thank you for reading!