I’ve written few forms with validation at work, recently. Since I like to work with libraries, especially those that helped me with this, I would like to introduce you to them.

Let’s build a simple Sign Up form for demonstration. Such form contains usually user’s e-mail, password, password confirmation and agreement with terms and conditions.

Formik

At first, we create Sign Up form with Formik library. I like Formik because it is lightweight, almost no abstraction library to help you create forms in React. If you don’t need Redux (You Might Not Need Redux — Dan Abramov — Medium, Should you store your form state in Redux? — Gosha Arinich) it is a very good choice. In our example, Formik helps us to keep state (values, errors and whether the form is being submitted) and handle changes. It also calls validation function for us on every submit. I use Formik as a React component with render prop, but you can also use it as HoC.

export default function SignUpFormContainer() {

return (

<Formik

initialValues={initialValues}

validate={validate}

onSubmit={onSubmit}

render={SignUpForm}

/>

)

} function SignUpForm(props) {

const {

isSubmitting,

errors,

handleChange,

handleSubmit,

} = props return (

<div className="form">

<label className="form-field" htmlFor="email">

<span>E-mail:</span>

<input

name="email"

type="email"

onChange={handleChange}

/>

</label>

<div className="form-field-error">{errors.email}</div> <label className="form-field" htmlFor="password">

<span>Password:</span>

<input

name="password"

type="password"

onChange={handleChange}

/>

</label>

<div className="form-field-error">{errors.password}</div> <label

className="form-field"

htmlFor="passwordConfirmation"

>

<span>Confirm password:</span>

<input

name="passwordConfirmation"

type="password"

onChange={handleChange}

/>

</label>

<div className="form-field-error">

{errors.passwordConfirmation}

</div> <label className="form-field" htmlFor="consent">

<span>Consent:</span>

<input

name="consent"

type="checkbox"

onChange={handleChange}

/>

</label>

<div className="form-field-error">{errors.consent}</div> <button onClick={handleSubmit}>

{isSubmitting ? 'Loading' : 'Sign Up'}

</button>

</div>

)

}

Validation

How do we validate this? We could probably find more approaches to how to validate such form. I wrote down some requirements we will use for validation of our form validation approach (Yes, it’s like the movie Inception all over again.)

Requirements

I don’t want to create special form field component and add validation to every field of the form separately like <FormField validate={value => someValidation(value)} /> . I think validation should be part of the business logic than part of the UI and I don’t want to mix it if it’s not necessary. Otherwise, I would like to have one validation function for whole form which takes an object of form values and returns object with error message for every value. This validation approach can even be used for whatever other JS object, not only as form validation.

. I think validation should be part of the business logic than part of the UI and I don’t want to mix it if it’s not necessary. Otherwise, I would like to have one validation function for whole form which takes an object of form values and returns object with error message for every value. This validation approach can even be used for whatever other JS object, not only as form validation. No if statements.

statements. There could be a possibility to replace if ’s with exceptions. I don’t like that either. Exceptions are for unexpected behaviour, but whether the form field is filled or not is not unexpected. On the contrary, it’s pretty expected. And like I’ve already mentioned, validation is part of our business rules.

’s with exceptions. I don’t like that either. Exceptions are for unexpected behaviour, but whether the form field is filled or not is not unexpected. On the contrary, it’s pretty expected. And like I’ve already mentioned, validation is part of our business rules. One or more rules for every field. I would like to validate each attribute against one or more rules.

I would also like to be able to return more errors for given field.

Return error message, different for every rule.

Cross-validation between attributes which means validation of field depending on other field value.

I mentioned no if s, because it could be tempting to start with something like this:

if (!values.email) {

errors.email = 'E-mail is required!'

} if (!values.password) {

errors.password = 'Password is required!'

} else if (values.password.length < 6) {

errors.password = 'Password has to be longer than 6 characters'

}

It could be very unreadable with more fields or more complicated rules, so I was thinking: “Can we do that better? There must be some other, more declarative way, without if s.”.

Yup

Fortunately, Formik itself allows to use Yup validation library by default. In the simplest way you can write just validationSchema and pass it as prop to <Formik /> component.

const validationSchema = Yup.object().shape({

email: Yup.string()

.email('E-mail is not valid!')

.required('E-mail is required!'),

password: Yup.string()

.min(6, 'Password has to be longer than 6 characters!')

.required('Password is required!')

}) <Formik

...

validationSchema={validationSchema}

...

/>

This looks much better than if s, but it works only until you need some cross validations. That’s the case of Password and Password Confirmation in our example.

In this case you can still use Yup, but together with your own validation function. We create getValidationSchema function. It takes values object and returns validation schema similar like in the example above.

function getValidationSchema(values) {

return Yup.object().shape({

email: Yup.string()

.email('E-mail is not valid!')

.required('E-mail is required!'),

password: Yup.string()

.min(6, 'Password has to be longer than 6 characters!')

.required('Password is required!'),

passwordConfirmation: Yup.string()

.oneOf([values.password], 'Passwords are not the same!')

.required('Password confirmation is required!'),

consent: Yup.bool()

.test(

'consent',

'You have to agree with our Terms and Conditions!',

value => value === true

)

.required(

'You have to agree with our Terms and Conditions!'

),

})

}

It’s pretty straightforward. I just want to highlight two points here:

passwordConfirmation validation: As far as I know Yup doesn’t have anything like equals method to compare two strings. We need to compare passwordConfirmation value with array values oneOf(array, 'Error message') where array is array with only one value from values object values.password . consent validation: Yup also doesn’t have method to check whether value is true or false . We need to use test method which takes 3 params. The first is name of the validation (I don’t know why 🤔), the second is error message and the third is our custom validation function which takes value as a parameter and must return true or false . We I use just simple value => value === true .

Next, we just call method validateSync() on this schema:

function validate(values) {

validate = (values) => {

const validationSchema = getValidationSchema(values)

try {

validationSchema.validateSync(values, { abortEarly: false })

return {}

} catch (error) {

return getErrorsFromValidationError(error)

}

}

}

There is a little problem, because validateSync method doesn’t return object with errors, but throws ValidationError . It would be better if it returned errors object directly. Anyway, I wrote another simple function which takes this error object and returns errors object in the form we want (with just first validation error message for every form field):

function getErrorsFromValidationError(validationError) {

const FIRST_ERROR = 0

return validationError.inner.reduce((errors, error) => {

return {

...errors,

[error.path]: error.errors[FIRST_ERROR],

}

}, {})

}

As you can see our validation looks now much better. Yes, we had to add two more functions (and yes, we’re handling validation by exception 🙄), but these are simple functions and can be re-used for whatever validation schema. If I could choose between if s and the declarative validation schema with two other functions, I would definitely go for the second option.

Pros:

Declarative

Predefined validation methods ( email , min , max , required )

Cons:

You have to learn its API

Validation handled by exception

Spected

Great thing about Formik is that you can use whatever validation library you want. You just need to use validate function instead of Yup specific validationSchema . I realise it would be good to use Spected validation library also for form validation, because we’re already using it for the JS objects’ validation on the backend in our project at work.

Spected validation is also very declarative. It’s even more functional than Yup. No imperative if s, no exception handling, just simple, declarative validation with functions.

Let’s start again with getValidationSchema function:

function getSpectedValidationSchema(values) {

return {

email: [

[value => !isEmpty(value), 'E-mail is required!'],

[value => isEmail(value), 'E-mail is not valid!'],

],

password: [

[value => !isEmpty(value), 'Password is required!'],

[

value => value.length >= 6,

`Password has to be longer than 6 characters!`,

],

],

passwordConfirmation: [

[

value => !isEmpty(value),

'Password confirmation is required!',

],

[

value => value === values.password,

'Passwords are not the same!',

],

],

consent: [

[

value => value === true,

'You have to agree with our Terms and Conditions!',

],

],

}

}

As you can see we need to use our own isEmpty and isEmail function, but I could be also beneficial, because we can define and name function to be more convenient for our business domain.

Our validate function looks little bit different. Now, we’re not handling exception, but we still have to process validation result to conform our requirement (object of errors with form field as the key and error message as the value):

function validate(getValidationSchema) {

return (values) => {

const spec = getValidationSchema(values)

const validationResult = spected(spec, values)

return getErrorsFromValidationResult(validationResult)

}

} function getErrorsFromValidationResult(validationResult) {

const FIRST_ERROR = 0

return Object.keys(validationResult).reduce((errors, field) => {

return validationResult[field] !== true

? { ...errors, [field]: validationResult[field][FIRST_ERROR] }

: errors

}, {})

}

There is one problem with Spected — the definition of required form fields. For example, if the values object doesn’t contain consent then validation function for the consent is not called. That’s because Spected does not iterate over undefined attributes.

This is not a problem in our Sign Up form, because we defined all values as initial state. In other situations, we can solved this by defining all required fields as undefined and override them with values from validated object:

const requiredFields = {

email: undefined,

password: undefined,

passwordConfirmation: undefined,

consent: undefined,

} const valuesWithRequiredFields = { ...requiredFields, ...values }

Pros:

Declarative, but more functional approach.

Simple lightweight API.

Validation is not handled by exceptions.

Cons:

There is no check for required fields which are not contained in validated object.

No predefined validation functions.

You can find whole code example here: GitHub — jakubkoci/react-form-validation: Simple React form validation with Formik and Yup.