Form Validation
React-admin relies on react-hook-form for the validation of user input in forms. React-admin supports several approaches:
- using the
validate
prop at the Form level (validation by function) - using the
validate
prop at the Input level - using the
resolver
prop at the Form level (validation by schema) - using the return value from the server (server-side validation)
You can’t use both form level validation and input level validation - this is a react-hook-form
limitation.
Validation Mode
By default, the validation mode is onSubmit
, and the re-validation mode is onChange
.
Since <Form>
actually passes all additional props to react-hook-form’s useForm
hook, this can easily be changed by setting the mode
and reValidateMode
props.
export const UserCreate = () => (
<Create>
<SimpleForm mode="onBlur" reValidateMode="onBlur">
<TextInput label="First Name" source="firstName" validate={required()} />
</SimpleForm>
</Create>
);
Global Validation
The value of the form validate
prop must be a function taking the record as input, and returning an object with error messages indexed by field. For instance:
const validateUserCreation = (values) => {
const errors = {};
if (!values.firstName) {
errors.firstName = 'The firstName is required';
}
if (!values.age) {
// You can return translation keys
errors.age = 'ra.validation.required';
} else if (values.age < 18) {
// Or an object if the translation messages need parameters
errors.age = {
message: 'ra.validation.minValue',
args: { min: 18 }
};
}
// You can add a message for a whole ArrayInput
if (!values.children || !values.children.length) {
errors.children = 'ra.validation.required';
} else {
// Or target each child of an ArrayInput by returning an array of error objects
errors.children = values.children.map(child => {
const childErrors = {};
if (!child || !child.firstName) {
childErrors.firstName = 'The firstName is required';
}
if (!child || !child.age) {
childErrors.age = 'ra.validation.required'; // Translation keys are supported here too
}
return childErrors;
});
}
return errors
};
export const UserCreate = () => (
<Create>
<SimpleForm validate={validateUserCreation}>
{/*
We need to add `validate={required()}` on required fields to append a '*' symbol
to the label, but the real validation still happens in `validateUserCreation`
*/}
<TextInput label="First Name" source="firstName" validate={required()} />
<TextInput label="Age" source="age" validate={required()} />
<ArrayInput label="Children" source="children" fullWidth validate={required()}>
<SimpleFormIterator>
<TextInput label="First Name" source="firstName" validate={required()} />
<TextInput label="Age" source="age" validate={required()} />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</Create>
);
Tip: The props you pass to <SimpleForm>
and <TabbedForm>
are passed to the useForm hook of react-hook-form
.
Tip: The validate
function can return a promise for asynchronous validation. See the Server-Side Validation section below.
Per Input Validation: Built-in Field Validators
Alternatively, you can specify a validate
prop directly in <Input>
components, taking either a function or an array of functions. React-admin already bundles a few validator functions, that you can just require, and use as input-level validators:
required(message)
if the field is mandatory,minValue(min, message)
to specify a minimum value for integers,maxValue(max, message)
to specify a maximum value for integers,minLength(min, message)
to specify a minimum length for strings,maxLength(max, message)
to specify a maximum length for strings,number(message)
to check that the input is a valid number,email(message)
to check that the input is a valid email address,regex(pattern, message)
to validate that the input matches a regex,choices(list, message)
to validate that the input is within a given list,unique()
to validate that the input is unique (seeuseUnique
),
Example usage:
import {
required,
minLength,
maxLength,
minValue,
maxValue,
number,
regex,
email,
choices
} from 'react-admin';
const validateFirstName = [required(), minLength(2), maxLength(15)];
const validateEmail = email();
const validateAge = [number(), minValue(18)];
const validateZipCode = regex(/^\d{5}$/, 'Must be a valid Zip Code');
const validateGender = choices(['m', 'f', 'nc'], 'Please choose one of the values');
export const UserCreate = () => (
<Create>
<SimpleForm>
<TextInput label="First Name" source="firstName" validate={validateFirstName} />
<TextInput label="Email" source="email" validate={validateEmail} />
<TextInput label="Age" source="age" validate={validateAge}/>
<TextInput label="Zip Code" source="zip" validate={validateZipCode}/>
<SelectInput label="Gender" source="gender" choices={[
{ id: 'm', name: 'Male' },
{ id: 'f', name: 'Female' },
{ id: 'nc', name: 'Prefer not say' },
]} validate={validateGender}/>
</SimpleForm>
</Create>
);
Tip: If you pass a function as a message, react-admin calls this function with { args, value, values,translate, ...props }
as argument. For instance:
const message = ({ translate }) => translate('myroot.validation.email_invalid');
const validateEmail = email(message);
Per Input Validation: Custom Function Validator
You can also define your own validator functions. These functions should return undefined
when there is no error, or an error string.
const required = (message = 'Required') =>
value => value ? undefined : message;
const maxLength = (max, message = 'Too short') =>
value => value && value.length > max ? message : undefined;
const number = (message = 'Must be a number') =>
value => value && isNaN(Number(value)) ? message : undefined;
const minValue = (min, message = 'Too small') =>
value => value && value < min ? message : undefined;
const ageValidation = (value, allValues) => {
if (!value) {
return 'The age is required';
}
if (value < 18) {
return 'Must be over 18';
}
return undefined;
};
const validateFirstName = [required(), maxLength(15)];
const validateAge = [required(), number(), ageValidation];
export const UserCreate = () => (
<Create>
<SimpleForm>
<TextInput label="First Name" source="firstName" validate={validateFirstName} />
<TextInput label="Age" source="age" validate={validateAge}/>
</SimpleForm>
</Create>
);
React-admin will combine all the input-level functions into a single function looking just like the previous one.
Input validation functions receive the current field value and the values of all fields of the current record. This allows for complex validation scenarios (e.g. validate that two passwords are the same).
Tip: If your admin has multi-language support, validator functions should return message identifiers rather than messages themselves. React-admin automatically passes these identifiers to the translation function:
// in validators/required.js
const required = () => (value, allValues, props) =>
value
? undefined
: 'myroot.validation.required';
// in i18n/en.json
export default {
myroot: {
validation: {
required: 'Required field',
}
}
}
If the translation depends on a variable, the validator can return an object rather than a translation identifier:
// in validators/minLength.js
const minLength = (min) => (value, allValues, props) =>
value.length >= min
? undefined
: { message: 'myroot.validation.minLength', args: { min } };
// in i18n/en.js
export default {
myroot: {
validation: {
minLength: 'Must be %{min} characters at least',
}
}
}
See the Translation documentation for details.
Tip: Make sure to define validation functions or array of functions in a variable outside your component, instead of defining them directly in JSX. This can result in a new function or array at every render, and trigger infinite rerender.
const validateStock = [required(), number(), minValue(0)];
export const ProductEdit = () => (
<Edit>
<SimpleForm defaultValues={{ stock: 0 }}>
...
{/* do this */}
<NumberInput source="stock" validate={validateStock} />
{/* don't do that */}
<NumberInput source="stock" validate={[required(), number(), minValue(0)]} />
...
</SimpleForm>
</Edit>
);
Tip: The props of your Input components are passed to a react-hook-form
useController hook.
Tip: The custom validator function can return a promise, e.g. to use server-side validation. See next section for details.
Async Validation
You can validate the entire form by returning a Promise in the form validate
function. For instance:
const validateUserCreation = async (values) => {
const errors = {};
if (!values.firstName) {
errors.firstName = 'The firstName is required';
}
if (!values.age) {
errors.age = 'The age is required';
} else if (values.age < 18) {
errors.age = 'Must be over 18';
}
const isEmailUnique = await checkEmailIsUnique(values.email);
if (!isEmailUnique) {
// Return a message directly
errors.email = 'Email already used';
// Or a translation key
errors.email = 'myapp.validation.email_not_unique';
// Or an object if the translation needs parameters
errors.email = {
message: 'myapp.validation.email_not_unique',
args: { email: values.email }
};
}
return errors
};
export const UserCreate = () => (
<Create>
<SimpleForm validate={validateUserCreation}>
<TextInput label="First Name" source="firstName" />
<TextInput label="Email" source="email" />
<TextInput label="Age" source="age" />
</SimpleForm>
</Create>
);
Per Input validators can also return a Promise to call the server for validation. For instance:
const validateEmailUnicity = async (value) => {
const isEmailUnique = await checkEmailIsUnique(value);
if (!isEmailUnique) {
return 'Email already used';
// You can return a translation key as well
return 'myroot.validation.email_already_used';
// Or even an object just like the other validators
return { message: 'myroot.validation.email_already_used', args: { email: value } }
}
return undefined;
};
const emailValidators = [required(), validateEmailUnicity];
export const UserCreate = () => (
<Create>
<SimpleForm>
...
<TextInput label="Email" source="email" validate={emailValidators} />
...
</SimpleForm>
</Create>
);
Schema Validation
react-hook-form
supports schema validation with many libraries through its resolver
props. The supported libraries are:
- Yup,
- Zod,
- Superstruct,
- Joi,
- Vest,
- class-validator,
- io-ts,
- Nope
- computed-types
- typanion,
- AJV,
- TypeBox,
- ArkType, and
- Valibot.
To use schema validation, use the resolver
prop following react-hook-form’s resolvers documentation. Here’s an example using yup
:
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { SimpleForm, TextInput, NumberInput } from 'react-admin';
const schema = yup
.object()
.shape({
name: yup.string().required(),
age: yup.number().required(),
})
.required();
const CustomerCreate = () => (
<Create>
<SimpleForm resolver={yupResolver(schema)}>
<TextInput source="name" />
<NumberInput source="age" />
</SimpleForm>
</Create>
);
Server-Side Validation
Server-side validation is supported out of the box for pessimistic
mode only. It requires that the dataProvider throws an error with the following shape:
{
"body": {
"errors": {
// Global validation error message (optional)
"root": { "serverError": "Some of the provided values are not valid. Please fix them and retry." },
// Field validation error messages
"title": "An article with this title already exists. The title must be unique.",
"date": "The date is required",
"tags": { "message": "The tag 'agrriculture' doesn't exist" },
}
}
}
Tip: The shape of the returned validation errors must match the form shape: each key needs to match a source
prop. The only exception is the root.serverError
key, which can be used to define a global error message for the form.
Tip: The returned validation errors might have any validation format we support (simple strings, translation strings or translation objects with a message
attribute) for each key. However root.serverError
does not accept translation objects.
Tip: If your data provider leverages React Admin’s httpClient
, all error response bodies are wrapped and thrown as HttpError
. This means your API only needs to return an invalid response with a json body containing the errors
key.
import { fetchUtils } from "react-admin";
const httpClient = fetchUtils.fetchJson;
const apiUrl = 'https://my.api.com/';
/*
Example response from the API when there are validation errors:
{
"errors": {
"root": { "serverError": "Some of the provided values are not valid. Please fix them and retry." },
"title": "An article with this title already exists. The title must be unique.",
"date": "The date is required",
"tags": { "message": "The tag 'agrriculture' doesn't exist" },
}
}
*/
const myDataProvider = {
create: (resource, params) =>
httpClient(`${apiUrl}/${resource}`, {
method: 'POST',
body: JSON.stringify(params.data),
}).then(({ json }) => ({
data: { ...params.data, id: json.id },
})),
}
Tip: If you are not using React Admin’s httpClient
, you can still wrap errors in an HttpError
to return them with the correct shape:
import { HttpError } from 'react-admin'
const myDataProvider = {
create: async (resource, { data }) => {
const response = await fetch(`${process.env.API_URL}/${resource}`, {
method: 'POST',
body: JSON.stringify(data),
});
const body = response.json();
/*
body should be something like:
{
errors: {
root: { serverError: "Some of the provided values are not valid. Please fix them and retry." },
title: "An article with this title already exists. The title must be unique.",
date: "The date is required",
tags: { message: "The tag 'agrriculture' doesn't exist" },
}
}
*/
if (status < 200 || status >= 300) {
throw new HttpError(
(body && body.message) || status,
status,
body
);
}
return body;
}
}