Custom validators asynchronous
In some scenarios you need to implement async validations, that's it a validation that won't return the result straight forward, for instance a validation that need to make a request to a rest-api and cannot provide the result until it gets the response from server.
Prior to learn about async validations, take your time and learn how to deal with synchronous validations there are various topics common to both validators that are explained in the previous topic section.
Async Field Validator
An async field validator is just a funcion that expects an argument (this argument is an object that contains several fields), and returns a promise that will contain a validation result once resolved.
Depending on the use case (e.g. is hitting a global rest api or a given domain specific rest api), it could be reusable or not.
Disecting an async validators
The definition of an asynchronous field validator:
- FieldValidationArgs: This is passed as a single argument in the validator
- value: current field value.
- values: form / record values (all fields).
- customArgs: we can pass custom arguments to our validator, this allow us to create flexible and configurable validators (e.g. in a min-lenght validator we can pass as customArgument a number indicating the minlenght allowed, or if we have a password / repeat password validator we can pass the id of the password field to the repeat password field).
- message: you can override here the error message that the validator returns in case of failure. You can inject in a message parameters to be interpolated by the validator (e.g. 'string must be at least {{minlength}}'), in some validators there may be more than one error message defined, in that case we can pass an array of strings.
- Promise<ValidationResult>: once the validator has been executed it returns a Promise<ValidationResult>,
once the promise gets resolved:
- If the validation succeeds, you get as return value a ValidationResult where it's field succeeded is true.
- If the validation fails, you get as return value a ValidationResult where it's field succeeded is false, a message where you get the error message (user friendly) and a type field that indicates the validator that failed.
export interface FieldValidatorArgs {value: any;values?: any;customArgs?: any;message?: string | string[];}export interface ValidationResult {type: string;succeeded: boolean;message: string;}export type FieldValidationFunctionAsync = (fieldValidatorArgs: FieldValidatorArgs) => Promise<ValidationResult>;
Learning by Example
The best way to learn how to implementing an async validator is just by doing it so.
Let's get started , we have a signup field and we ask the user to enter a new user id, this user Id has to be new, it cannot exists in our system (in this case we will check that the userId does not exists in github using the Github rest api).
We will start simple, in order to check we only need use the value we don't need the rest of optional params: values, customArgs, message, we will follow a TDD like approach we will create the validator always failing (returning a failed validation result).
const validatorType = 'GITHUB_USER_EXISTS';export const myValidator = fieldValidatorArgs => {const validationResult = {succeeded: false,type: validatorType,message: 'The username exists on Github',};return Promise.resolve(validationResult);};
Cool, we got a validator that always fails, now is time to inject our logic; we just want to validate that the login Id doesn't exists on Github.
const validatorType = 'GITHUB_USER_EXISTS';export const myValidator = fieldValidatorArgs => {+ const { value } = fieldValidatorArgs;const validationResult = {succeeded: false,type: validatorType,message: 'The username exists on Github',};- return Promise.resolve(validationResult);+ return fetch(`https://api.github.com/users/${value}`).then(response => {+ // Status 404, User does not exists, so the given user is valid+ // Status 200, meaning user exists, so the given user is not valid+ return response.status === 404+ ? {+ ...validationResult,+ succeeded: true,+ message: '',+ }+ : validationResult;+ });};
Let's add some additional changes to allow the error message to be customized (you can check an step by step guided solution in the synchronous validation section).
const validatorType = 'GITHUB_USER_EXISTS';+ let defaultMessage = 'The username exists on Github';+ export const setErrorMessage = message => (defaultMessage = message);export const myValidator = fieldValidatorArgs => {- const { value } = fieldValidatorArgs;+ const { value, message = defaultMessage } = fieldValidatorArgs;const validationResult = {succeeded: false,type: validatorType,- message: 'The username exists on Github',+ message,};return fetch(`https://api.github.com/users/${value}`).then(response => {// Status 404, User does not exists, so the given user is valid// Status 200, meaning user exists, so the given user is not validreturn response.status === 404? {...validationResult,succeeded: true,message: '',}: validationResult;});};
Check here:
Asynchronous Record Validator
An asynchronous record validator is a validation that is not tied up to an specific field, is usually something that we trigger when the user hits submit, and previous to send the information to the server.
Record validations usually are not highly reusable functions, they are tied up to the domain of the form to be evaluated.
Disecting a record validator
The definition of an asynchronous record validation:
- RecordValidationArgs: This is passed as a single argument in the validator.
- values: form / record values (all fields).
- message: you can override here the error message that the validator returns in case of failure. You can inject in a message parameters to be interpolated by the validator (e.g. 'string must be at least {{minlength}}'), in some validators there may be more than one error message defined, in that case we can pass an array of strings.
- Promise<ValidationResult>: once the validator has been executed it returns a Promise<ValidationResult>,
once the promise gets resolved:
- If the validation succeeds, you get as return value a ValidationResult where it's field succeeded is true.
- If the validation fails, you get as return value a ValidationResult where it's field succeeded is false, a message where you get the error message (user friendly) and a type field that indicates the validator that failed.
export interface RecordValidatorArgs {values: any;message?: string | string[];}export interface ValidationResult {type: string;succeeded: boolean;message: string;}export type RecordValidationFunctionAsync = (recordValidatorArgs: RecordValidatorArgs) => Promise<ValidationResult>;
Learning by Example
We have the following scenario: a third party rest-api where we can submit requests: this request will be queued up and it could take minutes or hours to get resolved (prior to process data this api's will check against a local database in real time in order to check if the information submitted is correct).
This could impact in a severe way to the usability of the application, our backend developers have decided to implement system that will hit a cache, and provide a real time response that in 90% of the case will be accurate (or at least will trap 90% of the user form errors).
The model of the form that we want to manage is:
interface Process {name: string;cachedResult: string;}
The server validation call will have the following signature (we will emulate it in our example).
const resolveProcess = (): Promise<string> => {const time = Math.random() * 1000;return time <= 900 ? '✅' : '❌';};
The validator that we will implement:
import { resolveProcessFromBackend } from './api';export const processValidator = ({ values }) =>resolveProcessFromBackend().then(data => {const succeeded = values.cachedResult === data;return {succeeded,message: succeeded? '': `Please, review the process. The real result was ${data}`,type: 'RECORD_PROCESS',};});
Check here:
Next steps
You have successfully completed the custom validators module.
You can choose wether to jump into React Final Forms Integration.