DEV Community

Oheneba Poku-Marboah
Oheneba Poku-Marboah

Posted on • Edited on

Yup.js validation in React without a third library

This article outlines how to do input field validation in a React application using Yup.js but without employing any third library to glue them together. Commonly, the Formik library is used to connect the Yup.js to React, but I wanted to do without that. I spent some hours trying to figure that out, and voila! This article shows how I went about achieving this.

According to the official Yup.js docs,

"Yup is a schema builder for runtime value parsing and validation. Define a schema, transform a value to match, assert the shape of an existing value, or both. Yup, schemas are extremely expressive and allow modelling complex, interdependent validations, or value transformation."

Yup.js is capable of a lot more than what I will show. My specific goal was to figure out how to do what is discussed in this article, so I only focused on that narrow part of Yup.js.
What is a schema (SKEE-mah)? I can imagine this is one of those English words messing with non-native speakers and probably native speakers too - btw it also has a weird plural form - schemas or schemata. According to the Cambridge Dictionary, a schema is a drawing that represents an idea or theory and makes it easier to understand. In computer science (CS), schema means a number of things, but the most common usage is in the realm of databases, where the schema is the blueprint of how the data in the database is structured.
In the case of Yup.js, schema simply means the predefined model (shape, restrictions) of what we want some information to look like. We want to register a kid for daycare, so we need their name and age. The name has to be longer than three characters, and the kid needs to be over one but at most five years old.

For my purposes, Yup.js is a validation library - I use it to assert that the values that are received conform to the shape and rules of a predefined ruleset. I display an error message under the form's input when the values are invalid. Then, I also make the borders of the input red, signifying the error state, based on a boolean that is set to false when the value is not valid. The state object kidErrors has both the boolean and the error message. FYI, this is a Next.js js project that uses material UI, TailwindCSS and Yup.js, so install the necessary packages. Alternatively, it can be done on any react project. You do not need the MaterialUI or the TailwindCSS, but you need a react app and Yup.js installed to try this out hands-on.

When I looked up examples of validating React inputs with Yup.js, most used third-party libraries, such as Formik, to connect the React inputs and Yup.js. However, I wanted to do this without the third-party connector library, so this is how I did it.

Sections\KidSection.tsx

import CustomTextField from '@/components/CustomTextField';
import React, { useState } from 'react';
export type Kid = { age?: number, name: string }
export type KidErrors = {
  age: boolean;
  ageErrMsg: string;
  name: boolean;
  nameErrMsg: string; 
}

interface KidSectionProps {
  kidValues: Kid
  kidErrors: KidErrors
  onChangeKidValues: (fieldName: string, value: any) => void
 }

export default function KidSection(props: KidSectionProps) {
  const { kidValues, onChangeKidValues, kidErrors } = props;

  return (
    <div>
      <h2>Tell us about your kid?</h2>
      <p> How old are they and what is their name</p>

      <div>

        <div className="sm:w-full md:w-1/4">
          <CustomTextField 
            autoFocus
            margin="dense"
            id="name"
            name='name'
            label="Name"
            type="text"
            required
            fullWidth
            isValid={kidErrors.name}
            errorMessage={kidErrors.nameErrMsg}
            value={kidValues.name}
            customOnChange={onChangeKidValues}  
          />
        </div>
        <div className="sm:w-full md:w-1/4" >
          <CustomTextField
            autoFocus
            margin="dense"
            id="age"
            name='age'
            label="Age"
            type="number"
            required
            fullWidth
            isValid={kidErrors.age}
            errorMessage={kidErrors.ageErrMsg}
            value={kidValues.age}
            customOnChange={onChangeKidValues}          
          />
        </div>

      </div>
    </div>

  );
}

Enter fullscreen mode Exit fullscreen mode

components\CustomTextField.tsx


import { StandardTextFieldProps, TextField } from '@mui/material';
import React, { ChangeEvent, FocusEvent } from 'react';

interface CustomTextFieldProps extends StandardTextFieldProps {
  isValid: boolean;
  errorMessage: string;
  customOnChange:  (fieldName: string, value: any) => void
}

export default function CustomTextField(props: CustomTextFieldProps) {
  const { name, onChange, errorMessage, isValid, customOnChange, ...rest } = props;

  const [value, setValue] = React.useState('');

  const handleOnChange = (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    setValue(event.target.value);

    if (customOnChange) customOnChange(name || '', event.target.value);
  };

  return (
    <TextField 
      name={name}
      value= {value}
      onChange={handleOnChange}
      helperText= {errorMessage}
      error={!isValid}
      {...rest}

    />
  );
}

Enter fullscreen mode Exit fullscreen mode

index.tsx

import KidSection, { Kid } from '@/Sections/KidSection';
import { generateKidSchema } from '@/utils/schemas';
import { useState } from 'react';
import { ValidationError } from 'yup';

const defaultKidErrors = { age: true, ageErrMsg: ' ', name: true, nameErrMsg: ' ' };


export default function Home() {
  const [kidValues, setKidValues] = useState<Kid>({ name: '', age: 3 });
  const [kidErrors, setKidErrors] = useState<typeof defaultKidErrors>(defaultKidErrors);


  const handleKidValueChange= async (fieldName: string, value: number | string ) => {

    const kidSchema = generateKidSchema();
    const newKidValues= { ...kidValues, [fieldName]: value };
    setKidValues(newKidValues);

      const handleKidValueChange= async (fieldName: string, value: number | string ) => {
        const kidSchema = generateKidSchema();
        const newKidValues= { ...kidValues, [fieldName]: value };
        setKidValues(newKidValues);    

        kidSchema.validate(newKidValues, { abortEarly: false }).catch((err: ValidationError) => {
          const errorMessages = err.inner.reduce((acc, curVal) => {
            const tempErrMsg = { [`${curVal.path}`]: false, [`${curVal.path}ErrMsg`]: curVal.message };

            return { ...acc, ...tempErrMsg  };
          }, {});
            // resets all values to as if the validation passed and then adds the current errors
          setKidErrors({ ...defaultKidErrors, ...errorMessages });
        });
      };
  };

  return (
    <div className='p-5'>
      <KidSection
        kidValues={kidValues}
        onChangeKidValues={handleKidValueChange}
        kidErrors={kidErrors}/>

    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

When a value changes, we update it in the handleKidValueChange function. We update the Kid value in the state, then pass the updated kid object to the validator to test whether it conforms to our schema expectations. If it doesn't conform to what is described in the schema, we want to update the error message to match the error being produced. If there were a submit button, the logic for enabling and disabling it would fit here.

utils\schemas.ts

import { ObjectSchema, number, object, string } from 'yup';

export const generateKidSchema = (maxAge: number= 5): ObjectSchema<Kid> => {
  return object({
    name: string().required().min(3, 'must be at least 3 characters long'),
    age: number().required().positive().integer().min(1).max(maxAge),
  });
};
Enter fullscreen mode Exit fullscreen mode

The yup schema I used is like this. I put it in a function because I may want to be able to change the maximum age on the fly if I need to. The restrictions we have set on the string and number are pretty basic, but Yup is capable of much more robust validation. It can be used to ensure that URLs, emails, and UUIDs are correct, ensure that string values don't include unnecessary whitespace (trim), validate the case of a string(upper/lower), validate one input based on the value of another input, among other more demanding things.

Erroneous validation

Sounds like it should make sense, right? WRONG! Something seems off. Things don't quite work the way we want it to work. The input field still has the error message even after I have typed in acceptable input. For example, if I start typing my name, "Oheneba", after I get to the third character, O-h-e, which is the minimum acceptable character amount, I still have an error message in the UI. Presently, it gives the impression that the validation doesn't work at all because it shows an error when the values are invalid, but when they are of the correct type, the UI errors don't change to a zero error state. So what could be wrong? What is supposed to happen when the data the user inputs is correct? We are supposed to not get an error at that point, right? If so, we are currently not updating the kidErrors to reflect a state of being valid when the input is indeed valid. In fact, we are missing the .then() method of the promise, so all we can do is check if our input value is correct or perform an action if it is wrong - no room has been left for an action to occur when the value is correct. Let's fix that.

### Corrected handleKidValueChange index.tsx

  const handleKidValueChange= async (fieldName: string, value: number | string ) => {
    const kidSchema = generateKidSchema();
    const newKidValues= { ...kidValues, [fieldName]: value };
    setKidValues(newKidValues);    

    kidSchema.validate(newKidValues, { abortEarly: false }).then((res) => {
      // No validation errors, clear the errors and set isValid to true
      console.log('res of validate then', res);
      setKidErrors({ ...defaultKidErrors });
    }).catch((err: ValidationError) => {
      // https://github.com/jquense/yup?tab=readme-ov-file#validationerrorerrors-string--arraystring-value-any-path-string
      const errorMessages = err.inner.reduce((acc, curVal) => {
        const tempErrMsg = { [`${curVal.path}`]: false, [`${curVal.path}ErrMsg`]: curVal.message };

        return { ...acc, ...tempErrMsg  };
      }, {});
        // resets all values to as if the validation passed and then adds the current errors
      setKidErrors({ ...defaultKidErrors, ...errorMessages });
    });
Enter fullscreen mode Exit fullscreen mode

We add the then() method of the returned promise to deal with that case. In that scenario, we clear the kidErrors message and set the error boolean state so the input field doesn't have a red outline. In the then() method of the promise, the correct full value is returned, but we don't need that value, so we can omit the parameter from our then() method's callback without any parameter. It should be working correctly now.

properly functioning validation

We have now understood that the Validate() function returns a promise containing the value when all is ok and throws a validation error when things are not ok; we could do our handleKidValueChange in another way. The try-catch is another way of doing the validation instead of using the .then().catch(). When the validated value is not up to standard, it throws an error, which can be handled in the catch() branch. You can immediately handle the case in the try branch if it is valid. Let's give that a shot - it will reinforce our understanding of how this part of Yup.js works.

handleKidValueChange using a try-catch and async-await


  const handleKidValueChange= async (fieldName: string, value: number | string ) => {
    const kidSchema = generateKidSchema();
    const newKidValues= { ...kidValues, [fieldName]: value };
    setKidValues(newKidValues);


  try {
        const validationResult = await kidSchema.validate(newKidValues, { abortEarly: false });
        console.log('validationResult in try catch ',validationResult);
        // No validation errors, clear the errors and set isValid to true
        setKidErrors({ ...defaultKidErrors });

      } catch (err: any) {
        // https://github.com/jquense/yup?tab=readme-ov-file#validationerrorerrors-string--arraystring-value-any-path-string
        const errorMessages = err.inner.reduce((acc: any, curVal: { path: any; message: any; }) => {
          const tempErrMsg = { [`${curVal.path}`]: false, [`${curVal.path}ErrMsg`]: curVal.message };

          return { ...acc, ...tempErrMsg  };
        }, {});
          // resets all values to as if the validation passed and then adds the current errors
        setKidErrors({ ...defaultKidErrors, ...errorMessages });
      }
    };

Enter fullscreen mode Exit fullscreen mode

I set out to illustrate that it is possible to validate input in React without using a third library to glue together Yup.js and the rendered React. In this article, I have demonstrated how to do some on-the-fly validation of inputs using Yup.js. I explained that it is a promise which returns the correct value when all is well but throws an error when the value passed does not match the schema's requirements. I also explained the branches in the code where the error logic can be handled and where you can wipe all error messages if the inputs are valid.

Top comments (0)