So you're like me and you love Server Actions, but you need more Zod and TypeScript!
- Server action validation with
zod-form-data
- Server error handling
- Amazing TypeScript DX
- Doesn't break
useActionState
progressive enhancement - No dependencies other then
zod
/zod-form-data
This library is designed to enhance useActionState
, which requires React 19. Without that, you really won't get much benefit from this library.
I have only tested with Next.js for now, but the goal is to make this work with React in general.
See christianjuth.github.io/better-react-server-actions.
"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('/examples/success')
}
});
"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>
)
}
"use server";
import { createActionWithState } from 'better-react-server-actions';
import { z } from 'zod';
export const login = createActionWithState({
stateSchema: z.object({
likeId: z.string().optional(),
}),
requestHandler: async ({ likeId }) => {
// Check if user is logged in
// and redirect to login page if not
if (likeId) {
// Delete like via api
return {
likeId: undefined,
}
} else {
// Create like via api
const newLikeId = 'new-like-id';
return {
likeId: newLikeId,
}
}
}
});
"use client";
import { useActionState } from 'react';
import { toggleLike } from './action';
export default function Page() {
const [state, action] = useActionState(toggleLike, {});
return (
<form action={action}>
<button>
{state.likeId ? 'Unlike' : 'Like'}
</button>
</form>
)
}
"use server";
import { createActionWithState } from 'better-react-server-actions';
import { z } from 'zod';
export const incrementCounter = createActionWithState({
stateSchema: z.object({
count: z.number().min(0),
}),
requestHandler: async ({ count }) => {
return {
count: count + 1,
}
}
});
"use client";
import { useActionState } from 'react';
import { incrementCounter } from './action';
export default function Page() {
const [state, action] = useActionState(incrementCounter, {
count: 0,
});
return (
<form action={action}>
<span>Count: {state.count}</span>
<button>
Increment
</button>
</form>
)
}