API
createActionWithState

createActionWithState

// action.ts
import { createActionWithState } from 'better-react-server-actions';
export default createActionWithState(config);

Config

config.formDataSchema (optional)

zod-form-data (opens in a new tab) schema for validating formData.

import { createActionWithState } from 'better-react-server-actions';
import { zfd } from 'zod-form-data';
 
export const formAction = createActionWithState({
  formDataSchema: zfd.formData({
    email: zfd.text(),
  }),
});

config.stateSchema (optional)

zod (opens in a new tab) schema for validating state.

import { createActionWithState } from 'better-react-server-actions';
import { z } from 'zod';
 
export const formAction = createActionWithState({
  stateSchema: z.object({
    counter: z.number().min(0),
  }),
});

config.requestHandler

A function that is called when action is triggered if form data validation passes.

import { createActionWithState } from 'better-react-server-actions';
 
export const formAction = createActionWithState({
  requestHandler: async (prevState, validatedFormData) => {
    // call api, access db directly, set cookies, throw errors, etc.
  },
});

config.formatServerError (optional)

Return a custom error message when requestHandler throws. This is great for mapping verbose database errors to user-friendly messages.

import { createActionWithState } from 'better-react-server-actions';
 
export const formAction = createActionWithState({
  requestHandler: async (prevState, validatedFormData) => {
    throw new Error('Database error');
  },
  formatServerError: (error) => {
    if (error.message === 'Database error') {
      return 'Custom error message';
    }
  },
});

Combine with useActionState

useActionState is a React hook that extends Server Actions by adding state, making them more interactive. However, it still enables the form to be submitted even before JavaScript is fully loaded.

"use client";
 
import formAction from './action.ts';
import { useActionState } from 'react'; // React 19+ required
 
function MyComponent() {
  const [state, action] = useActionState(formAction, {});
  return <form action={action}>...</form>;
}

State

State is the first element returned from useActionState.

  • errors?.actionErrors: an array of strings populated if requestHandler throws (details)
  • errors?.formErrors: an object of validation errors for each form field (details)
  • errors?.stateErrors: an object of validation errors for each state field (details)

state.errors.actionErrors

An array of strings populated if requestHandler throws.

"user server";
 
import { createActionWithState } from 'better-react-server-actions';
 
export const formAction = createActionWithState({
  requestHandler: async (prevState, validatedFormData) => {
    throw new Error('Server error');
  }
});
"use client";
 
import formAction from './action.ts';
import { useActionState } from 'react'; // React 19+ required
 
function Page() {
  const [state, action] = useActionState(formAction, {});
  return (
    <form action={action}>
      {state.errors?.actionErrors?.map((error) => (
        <div key={error}>{error}</div>
      ))}
      <button type="submit">Submit</button>
    </form>
  );
}

state.errors.formErrors

An object of validation errors for each form field.

"user server";
 
import { createActionWithState } from 'better-react-server-actions';
import { zfd } from 'zod-form-data';
 
export const formAction = createActionWithState({
  formDataSchema: zfd.formData({
    email: zfd.text(),
  }),
});
"use client";
 
import formAction from './action.ts';
import { useActionState } from 'react'; // React 19+ required
 
function Page() {
  const [state, action] = useActionState(formAction, {});
  return (
    <form action={action}>
      <input name="email" />
      {state.errors?.formErrors?.email?.map((error) => (
        <div key={error}>{error}</div>
      ))
      <button type="submit">Subscribe</button>
    </form>
  );
}

state.errors.stateErrors

An object of validation errors for each state field.

"user server";
 
import { createActionWithState } from 'better-react-server-actions';
import { z } from 'zod';
 
export const formAction = createActionWithState({
  stateSchema: z.object({
    counter: z.number().min(0),
  }),
  requestHandler: async (prevState, validatedFormData) => {
    return {
      counter: prevState.counter + 1,
    };
  },
});
"use client";
 
import formAction from './action.ts';
import { useActionState } from 'react'; // React 19+ required
 
function Page() {
  const [state, action] = useActionState(formAction, { counter: 0 });
  return (
    <form action={action}>
      <span>Count: {state.counter}</span>
      {state.errors?.stateErrors?.counter?.map((error) => (
        <div key={error}>{error}</div>
      ))
      <button type="submit">Increment</button>
    </form>
  );
}