Form submission
We recommend using Next.JS form actions to handle form submissions. The solution requires two parts:
- A server action
- The form component
Form action
When creating a server action form a form you should create it using the
FormAction type. the server action should:
- Convert the form data into a typed record of values
- Potentially validate the values submitted by the user
- Perform whatever action you need to perform (e.g. logging the user in)
- Return either a success or error state
When returning data you return the following fields:
status: Tells the frontend if the form submission was successful or notformData: The form data that was submitted, this is used to maintain the field values after a form submission.serverError: A server-level error message to display to the userfieldErrors: Field-level validation errors to display to the user
// login-form/actions.ts
import { login } from '@/api/auth';
import { FormAction } from '@/ui/components/forms/types';
import { getFormValues } from '@/ui/components/forms/util';
export const loginAction: FormAction = async (currentState, formData) {
const values = getFormValues<{ email: string; password: string }>(formData);
try {
await login(values.email, values.password);
} catch (error) {
// Could not log in so display a form-level error message
return {
status: 'error',
formData,
serverError: 'Failed to login',
};
}
return {
status: 'success',
formData: new FormData(), // clear the form data on success
};
}Form component
The form component:
- Passes the action to the form via
useFormAction(action) - Gets the default values from the action state to preserve input values when the form is submitted
- Passes field-level validation errors to the form
- Renders any server errors from the action
- Passes the default values to the form input components
// login-form/form.tsx
import { Form } from '@/ui/components/forms/form';
import { useFormAction } from '@/ui/components/forms/hooks/use-form-action';
import { loginAction } from './actions';
export const LoginForm = () => {
const form = useFormAction<{ email: string; password: string }>(loginAction);
return (
<Form
action={form.action}
validationErrors={form.fieldErrors} // Pass field-level validation errors to the form
>
<FormErrors>
{/* Render any server errors from the action */}
{form.serverError && !form.isPending && (
<ServerError title="Could not sign in">
{form.serverError}
</ServerError>
)}
{/* Render a summary of field level errors */}
<FieldErrorSummary
fieldLabels={{
email: 'Email Address',
password: 'Password',
}}
title="There are errors in your submission"
subtitle="Please correct your input in these fields:"
/>
</FormErrors>
<TextField
id="email"
name="email"
label="Email"
isRequired
autoComplete="email"
defaultValue={form.defaultValues.email}
/>
<TextField
id="password"
name="password"
label="Password"
isRequired
autoComplete="current-password"
defaultValue={form.defaultValues.password}
/>
<Button type="submit" isLoading={form.isPending}>
Login
</Button>
</Form>
);
};useFormAction(action)
The useFormAction(action) hook accepts a server action of type FormAction and returns data ready to be used in the form component:
action: The form action to be passed to the form componentdefaultValues: A value for each field that should be passed to thedefaultValueprop. This ensures that field values are not cleared when the form is submitted.fieldErrors: Field-level validation errors to display to the userserverError: A server-level error message to display to the userisPending: Whether the form submission is in progress
const form = useFormAction<{ email: string; password: string }>(loginAction);FormAction type
The FormAction type is a function that takes a current state and a form data and returns a promise of a form action state.
type FormAction = (
currentState: FormActionState,
formData: FormData,
) => Promise<FormActionState>;It can return one of the following states:
FormActionSuccessState: The form submission was successfulFormActionErrorState: The form submission was unsuccessfulFormActionIdleState: The form submission is pending
type FormActionSuccessState = {
status: 'success';
formData: FormData;
};
type FormActionErrorState = {
status: 'error';
formData: FormData;
serverError: string;
};
type FormActionIdleState = {
status: 'idle';
formData: FormData;
};getFormValues(formData)
This utility lets you convert a FormData instance into a typed record of values.
const values = getFormValues<{ email: string; password: string }>(formData);
values.email; // string
values.password; // string