Form submission

We recommend using Next.JS form actions to handle form submissions. The solution requires two parts:

  1. A server action
  2. 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 not
  • formData: 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 user
  • fieldErrors: 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 component
  • defaultValues: A value for each field that should be passed to the defaultValue prop. This ensures that field values are not cleared when the form is submitted.
  • fieldErrors: Field-level validation errors to display to the user
  • serverError: A server-level error message to display to the user
  • isPending: 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 successful
  • FormActionErrorState: The form submission was unsuccessful
  • FormActionIdleState: 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