Form validation
Before submitting a form, of course we'd like to check if the input is valid. Are all required fields filled out? Do we need a minimum or maximum amount of characters or are there other additional requirements (including numbers or special characters when setting up a password, for example)?
If the information is correctly formatted, the application allows the data to be submitted to the server and (usually) saved in a database; if the information isn't correctly formatted, it gives the user an error message explaining what needs to be corrected, and lets them try again.
There are different ways to validate data. We will focus on built-in validation and a server-side validation pattern that fits seamlessly with Next.js' Server Actions (which we will cover in the next chapter).
Built-in form validation
There are several HTML5 form validation features, which are very easy to use. We don't have to rely on JavaScript for this type of validation. There are various validation attributes that can be used on <input>
, <select>
or <text-area>
elements, such as:
required
minlength
andmaxlength
for stringsmin
andmax
for numerical input typestype
, to specify a preset type, such aspassword
oremail
See MDN for the many different types and other attributes.
For styling an invalid input element, we can use the :invalid
CSS-pseudo-class.
Server-side validation
To build a server-side validation schema, we use the Zod library. This library enables us to declare a schema to ensure that your form inputs have the correct type. On top of that you can specify a custom error message.
First, store your addCalendarClient
function in a actions.ts
file in the root of your app. Move the 'use server'
.
In your actions.ts
file, where you stored your Server Action declare the following schema:
'use server'
import { z } from 'zod'
const schema = z.object({
client: z.string()
type: z.string(),
startTimestamp: z.string().refine((date) => !isNaN(Date.parse(date)), {
message: 'Invalid date format',
}),
stopTimestamp: z.string().refine((date) => !isNaN(Date.parse(date)), {
message: 'Invalid date format',
}),
})
// Rest of the code...
This schema ensures that inputs are strings and that the startTimestamp
and stopTimestamp
also have a correct ISO string format. Please check the further reading section to learn more about the refine()
method of Zod. We check if the ISO string can actually be parsed by the Date
object. If the return value is falsy, we know that the ISO string format is invalid.
The schema can then be used within the Server Action to validate all the fields.
After you've done that you can validate your formData
easily in within Server Action like this:
export const createCalendarEvent = async (
initialState: any,
formData: FormData,
) => {
const date = formData.get('date');
const from = formData.get('from');
const to = formData.get('to');
const validatedFields = schema.safeParse({
client: formData.get('client'),
type: formData.get('activity'),
startTimestamp: `${date}T${from}:00.000Z`,
stopTimestamp: `${date}T${to}:00.000Z`,
});
if (!validatedFields.success) {
return validatedFields.error?.flatten().fieldErrors;
}
console.log('validatedFields', validatedFields.data);
};
Then, in your form component, a slight refactor is necessary to be able to show potential error messages. We make sure that the <Form />
becomes a client component, and then we import useActionState
. This hook allows us to show to access the state of the form, plus we have access to the pending
boolean. The form then looks something like this:
Note that when your Server Action is processed through the useActionState
, the first argument of the action is the initialState
and the second argument the formData
.
'use client'
import { useActionState } from 'react'
import { createCalendarEvent } from '@/app/actions'
const initialState = {
message: '',
}
export function Form() {
const [state, formAction, pending] = useActionState(createCalendarEvent, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Client</label>
<input id="client" name="client" required type="text" />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>Add event</button>
</form>
)
}
This way the validation messages can be shown easily, and we can disable the button conditionally with the pending
prop.