Our objective in this lesson is to display form errors on the client side.
{ "message": "The name field is required. (and 2 more errors)", "errors": { "name": [ "The name field is required." ], "email": [ "The email field is required." ], "password": [ "The password field is required." ] }}
Considering that we are going to have more forms in the future it is a good idea to have a component where we can pass errors object and a field name to display particular error messages under the specific field.
Now let's extend our useAuth
hook src/hooks/useAuth.jsx
by adding a new errors
state, this is where we are going to store the errors object returned from the backend.
The src/hooks/useAuth.jsx
file should look like this.
import { useState } from 'react'import { useNavigate } from 'react-router-dom'import { route } from '@/routes' export function useAuth() { const [errors, setErrors] = useState({}) const navigate = useNavigate() async function register(data) { setErrors({}) return axios.post('auth/register', data) .then(() => { navigate(route('vehicles.index')) }) .catch(error => { if (error.response.status === 422) { setErrors(error.response.data.errors) } }) } return { register, errors }}
When the server returns error code 422 Unprocessable entity we set the errors state with the values the server returned in the catch
clause.
setErrors(error.response.data.errors)
On every new submission, we want to reset the errors
state, this is done with the setErrors({})
line.
Create a new src/components/ValidationError.jsx
component.
import PropTypes from 'prop-types' function ValidationError({ errors, field }) { return errors?.[field]?.length && <div className="alert alert-danger" role="alert"> <ul> { errors[field].map((error, index) => { return (<li key={ index }>{ error }</li>) }) } </ul> </div>} ValidationError.propTypes = { errors: PropTypes.object.isRequired, field: PropTypes.string.isRequired,} export default ValidationError
Like we did with the NamedLink
component, we define properties errors
and field
that component will accept and display errors for a field where the object key matches.
src/views/auth/Register.jsx
component. To display error messages is pretty straightforward now.import { useState } from 'react'import { useAuth } from '@/hooks/useAuth'import ValidationError from '@/components/ValidationError' function Register() { const [name, setName] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [passwordConfirmation, setPasswordConfirmation] = useState('') const { register, errors } = useAuth() async function handleSubmit(event) { event.preventDefault() await register({ name, email, password, password_confirmation: passwordConfirmation }) setPassword('') setPasswordConfirmation('') } return ( <form onSubmit={ handleSubmit } noValidate> <div className="flex flex-col mx-auto md:w-96 w-full"> <h1 className="heading">Register</h1> <div className="flex flex-col gap-2 mb-4"> <label htmlFor="name" className="required">Name</label> <input id="name" name="name" type="text" value={ name } onChange={ event => setName(event.target.value) } className="form-input" autoComplete="name" /> <ValidationError errors={ errors } field="name" /> </div> <div className="flex flex-col gap-2 mb-4"> <label htmlFor="email" className="required">Email</label> <input id="email" name="email" type="email" value={ email } onChange={ event => setEmail(event.target.value) } className="form-input" autoComplete="email" /> <ValidationError errors={ errors } field="email" /> </div> <div className="flex flex-col gap-2 mb-4"> <label htmlFor="password" className="required">Password</label> <input id="password" name="password" type="password" value={ password } onChange={ event => setPassword(event.target.value) } className="form-input" autoComplete="new-password" /> <ValidationError errors={ errors } field="password" /> </div> <div className="flex flex-col gap-2"> <label htmlFor="password_confirmation" className="required">Confirm Password</label> <input id="password_confirmation" name="password_confirmation" type="password" value={ passwordConfirmation } onChange={ event => setPasswordConfirmation(event.target.value) } className="form-input" autoComplete="new-password" /> </div> <div className="border-t h-[1px] my-6"></div> <div className="flex flex-col gap-2 mb-4"> <button type="submit" className="btn btn-primary"> Register </button> </div> </div> </form> )} export default Register
Here we imported the ValidationError
component.
import ValidationError from '@/components/ValidationError'
Unpacked errors state from useAuth hook.
const { register, errors } = useAuth()
And added the ValidationError
component after each input value by passing the errors
state and field name which matches keys from the API response.
<ValidationError errors={ errors } field="name" /><ValidationError errors={ errors } field="email" /><ValidationError errors={ errors } field="password" />
password_confirmation
field never returns error messages, so we do not need to add it there. Password-related errors are always returned under thepassword
key.