Creating an Action

Let's create our first action!

In this example, we will create a simple login form that redirects to a success page if the email and password match.

"use server";
 
import { createActionWithState } from 'better-react-server-actions';
import { zfd } from 'zod-form-data';
import { z } from 'zod';
import { redirect } from 'next/navigation';
 
const EMAIL = 'admin@example.com';
const PASSWORD = 'password';
 
export const login = createActionWithState({
  formDataSchema: zfd.formData({
    email: z.string().email(),
    password: zfd.text(),
  }),
  requestHandler: async (prevState, { email, password }) => {
    if (email !== EMAIL || password !== PASSWORD) {
      throw new Error('Invalid email or password');
    }
 
    redirect('/')
  }
});
"use client";
 
import { useActionState } from 'react';
import { login } from './action';
 
export default function Page() {
  const [state, action] = useActionState(login, {});
 
  const formErrors = state.errors?.formErrors;
 
  return (
    <form action={action}>
      <h1>Login</h1>
 
      {state.errors?.actionErrors && (
        <span>
          {state.errors.actionErrors.join(', ')}
        </span>
      )}
 
      <label htmlFor="email">Email:</label>
      <input 
        id="email" 
        name="email" 
      />
      {formErrors?.email && (
        <span>
          {formErrors.email.join(', ')}
        </span>
      )}
 
      <label htmlFor="password">Password:</label>
      <input 
        id="password" 
        name="password" 
        type="password" 
      />
      {formErrors?.password && (
        <span>
          {formErrors.password.join(', ')}
        </span>
      )}
 
      <button>
        Login
      </button>
    </form>
  )
}

Making it better

We can prevent form data from clearing on sumit. Even if JavaScript is disabled, and React falls back to an html form submission, the form state will still be preserved!

"use client";
 
import { useActionState } from 'react';
import { login } from './action';
import { getPreviousFormData } from 'better-react-server-actions';
 
export default function Page() {
  const [state, action] = useActionState(login, {});
 
  const formData = getPreviousFormData(state);  
 
  const formErrors = state.errors?.formErrors;
 
  return (
    <form action={action}>
      <h1>Login</h1>
 
      {state.errors?.actionErrors && (
        <span>
          {state.errors.actionErrors.join(', ')}
        </span>
      )}
 
      <label htmlFor="email">Email:</label>
      <input 
        id="email" 
        name="email" 
        defaultValue={formData.get('email') ?? undefined}
      />
      {formErrors?.email && (
        <span>
          {formErrors.email.join(', ')}
        </span>
      )}
 
      <label htmlFor="password">Password:</label>
      <input 
        id="password" 
        name="password" 
        type="password" 
        defaultValue={formData.get('password') ?? undefined}
      />
      {formErrors?.password && (
        <span>
          {formErrors.password.join(', ')}
        </span>
      )}
 
      <button>
        Login
      </button>
    </form>
  )
}

Try it out

Open in new a tab (opens in a new tab)