createActionWithState
// action.ts
import { createActionWithState } from 'better-react-server-actions';
export default createActionWithState(config);
Config
formDataSchema
: (optional) zod-form-data (opens in a new tab) schema for validating formData (details)stateSchema
: (optional) zod (opens in a new tab) schema for validating state (details)requestHandler
: a function that is called when action is triggered if form data validation passes (details)formatServerError
: (optional) return a custom error message whenrequestHandler
throws (details)
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 ifrequestHandler
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>
);
}