In this article, we’re going to expand our forms API to support yup-based schema validations.

In part 1 of this series we investigated using React hooks to design a useForm React forms library. Part 1 covers the motivation and some overall design goals of the library.

In part 2 of this series we created a useInput hook, integrated it into our useForm hook, reviewed our solution’s state management, incorporated some additional tests, and reviewed testing strategies in more detail.

In part 3 we covered validation, asynchronous validation, and asynchronous form submits. It gave us the key elements of a React hooks based forms API.

In part 4 we scaled out our solution to support standard HTML input types, and custom inputs.

In part 5 we scaled useForm to support dynamic forms.

Working example

Let’s check out a demo of the useForm hook with yup schema validation support:

Demo of useForm with yup schema validation

The full solution is found in my GitHub. There is a lot of boilerplate in the demo, but I’d rather have the examples be very explicit at this point. Please note that tests don’t pass in CodeSandbox at this time due to a CodeSandbox design decision to not support loading devDependencies from package.json .

Yup schema validation overview and benefits

Yup is a schema validation library. You use it to define a JavaScript schema, and then validate an arbitrary object against that schema. Yup provides you with a full set of standard schema validations like required, min length, max length, email, etc.

There are a lot of schema validation libraries, yup happens to be one of the larger and more active ones. Let’s see what yup validation looks like by reviewing a code example:

const { object, string, number, date } = require('yup') const contactSchema = object({

name: string()

.required(),

age: number()

.required()

.positive()

.integer(),

email: string()

.email(),

website: string()

.url(),

createdOn: date()

.default(() => new Date())

})

name: 'jimmy',

age: 24,

email: '

} let contact = {name: 'jimmy',age: 24,email: ' jdog@cool.biz await contactSchema.isValid(contact) // true = valid

contactSchema is the yup schema. contact is your data object (this could be data from a form). Finally calling contactSchema.isValid() validates the contact object against the contactSchema yup schema. Yup provides a fluent API to define a schema. Realistically that might not be how you want to define your schemas.

For example, if your fields are dynamically created from a database, how would you store all of these fluent API calls? In those cases, you can use libraries that allow you to store your schema in JSON format, which can then be serialized and stored in any database. schema-to-yup is one such library that lets you define your schema in JSON, and then convert the schema to yup schema objects.

The useForm hook needs to integrate with yup. When yup schemas are provided to useForm , useForm will need to apply those validations to input fields.

Applying schema validations at arbitrary times

Just like our out of the box validations that we created in an earlier post, schema validations should be able to run at the same times that our out of the box validations can. We should allow form creators to say “hey I want schema validations on my first name field to run onBlur and onSubmit”, or “hey I want to apply schema validations to my first name field, and apply my reward early validate late design pattern at the same time”.

Designing the public API to support schema validations

In my opinion, forms are subsets of schemas. There is coupling between the two. So I would prefer to pass the schema with the initial useForm hook call.

export const useForm = ({ id, initialState = {}, validationSchema = {} }) => {

const inputValues = useRef({

...initialState

});

useForm now receives a validationSchema property that when set, will apply schema validations to the fields eventually defined within the form. To use schema validation, a form creator would do the following:

const { required, mustBeTrue, schema } = validators; const contactSchema = object({

firstName: string()

.required()

.min(3),

lastName: string()

.required()

.length(10),

email: string().email()

}); const {

getFormProps,

inputValues,

uiState,

api,

formValidity,

inputUiState

} = useForm({

id: "kitchenSinkForm",

initialState,

validationSchema: contactSchema

}); const firstNameInput = api.addInput({

id: "firstName",

value: inputValues.firstName,

validators: [

{

...schema,

when: [

{

eventType: onChange,

evaluateCondition: evaluateConditions.rewardEarlyValidateLate

},

onBlur,

onSubmit

]

}

]

}); const lastNameInput = api.addInput({

id: "lastName",

value: inputValues.lastName,

validators: [{ ...schema, when: [onBlur, onSubmit] }]

}); const emailInput = api.addInput({

id: "email",

value: inputValues.email,

validators: [

{ ...schema, when: [onBlur, onSubmit] }

// {

// ...email,

// when: [onBlur, onSubmit]

// }

]

});

In the sample form code above, we define contactSchema , which is a yup schema. We pass this to the useForm hook. Then, when we define our inputs, we indicate that we want the schema validations to be applied to them with the validators: [ { ...schema } ] validator. The nice part about this approach is that we can also define when we want those schema validations applied — onBlur, onSubmit, etc.

In api.addInput , when we specify that we want to use the schema validator, useForm connects the appropriate schema field based on the input id .

Implementation details

Creating the schema validator

In the useForm code above we used the schema validator. Here is the schema validator code from validators.js:

validators.schema = createValidator({

validateFn: async ({ value, validationSchema }) => {

if (!validationSchema || !validationSchema.validate) return true;

try {

await validationSchema.validate(value);

return true;

} catch (e) {

return false;

}

},

error: "INVALID_SCHEMA"

});

We’re using the yup validate API to perform the validation. We follow the same pattern of other validators — return true/false based on validation success. If a yup validation fails, yup will return many other properties and even a description of the error, but my preference is not to include content in the validation errors. So if schema validations fail, they will just return the INVALID_SCHEMA error message. Form creators can decide what copy to display in those cases. I believe this could be improved upon, but I’m not sure how because form creators have access to the schema definition, so they can query the schema definition to create an error message if they wish.

useForm will only pass one schema field to the schema validation. The expectation is that if we are validating the first name field, useForm will pass just the firstName field to the schema validator, instead of the full schema

Updating useForm to pass the schema field to the schema validator

useForm requires minimal changes. Now that form creators can pass validationSchema to useForm , anytime validations need to figure, useForm can pass the correct schema field to the validations. validators.js simply needs to take the validationSchema as an argument and pass it on to the schema validator when applicable.

const getSchemaField = (validationSchema, field) =>

validationSchema.fields ? validationSchema.fields[field] : null; ... const validateAll = async eventType => {

const promises = [];

let newUiState = { ...uiState }; Object.keys(validators.current).forEach(async field => {

promises.push(

runValidators({

field,

validators: validators.current[field],

eventType,

value: inputs.current[field].value,

inputValues: inputValues.current,

validationSchema: getSchemaField(validationSchema, field)

})

);

}); ... const runInputValidations = async ({ id, value, eventType, timeStamp }) => {

... try {

const validationResults = await runValidators({

field: id,

validators: validators.current[id],

eventType: eventType,

value,

runId: timeStamp,

inputValues: inputValues.current,

validationSchema: getSchemaField(validationSchema, id)

});

Summary

In this post, we refactored our solution to support yup-based schema validations.

In later parts, we’ll cover other topics such as reducing boilerplate, promise cancellations, debounce, hook performance considerations, and other design optimizations.

At this point, the library is probably just about “good enough” to use in production 😃. Keep in mind the effort and thought that needs to go into making a forms library just “good enough” before you embark on the journey of making a forms library yourself!

Unless you have an extremely simple form that requires no validation logic, yes, you can cobble a form together with React, but it will be very limited. Once you need to get past simple forms, use a forms library. Otherwise, you might not be making the best use of your time. Unless you’re doing it for your own learning purposes of course, which I always recommend learning by developing something that you’re interested in.

Let me know if you have any questions, feedback, or suggestions, or if you would like me to cover anything else within this series.