next-form-action
Version:
A TypeScript library for handling server actions in Next.js with type-safe error handling and callbacks
221 lines (166 loc) • 5.26 kB
Markdown
A TypeScript library for handling server actions in Next.js with type-safe error handling and callbacks.
- 🚀 **Type-safe server actions** with full TypeScript support
- 🎯 **Generic data handling** - work with any data type, not just FormData
- 🔄 **Automatic redirects and refresh** handling
- 🎨 **React hooks** for seamless integration with `useTransition`
- 🛡️ **Discriminated union types** for better type safety
- ⚡ **Next.js 16+ optimized** with proper error digest handling
- 🌐 **Dual module support** (ESM + CommonJS)
- 🔧 **Lifecycle callbacks** for success and error events
- ✨ **Global error/success functions** for clean action code
```bash
npm install next-form-action
yarn add next-form-action
pnpm add next-form-action
```
```typescript
import { createAction, error, success } from "next-form-action";
interface LoginData {
email: string;
password: string;
}
export const loginAction = createAction<LoginData>(
async (data) => {
// Validate input
if (!data.email || !data.password) {
error("Email and password are required");
}
// Authenticate user
const user = await authenticateUser(data.email, data.password);
if (!user) {
error("Invalid email or password.");
}
if (!user.isActive) {
error("Please check your inbox for activation email.");
}
// Success with redirect
success("Welcome back!", { redirect: "/dashboard" });
},
"loginAction", // context for logging
);
```
```tsx
"use client";
import { FormEvent } from "react";
import { useAction } from "next-form-action";
import { loginAction } from "./actions";
interface LoginData {
email: string;
password: string;
}
export default function LoginForm() {
const { dispatch, state, isPending } = useAction<LoginData>(loginAction, {
onSuccess: (result) => {
console.log("Logged in:", result.message);
},
onError: (result) => {
console.log("Login failed:", result.message);
},
});
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data: LoginData = {
email: formData.get("email") as string,
password: formData.get("password") as string,
};
dispatch(data);
};
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
{state?.message && <div className={state.success ? "success" : "error"}>{state.message}</div>}
<button type="submit" disabled={isPending}>
{isPending ? "Signing in..." : "Sign In"}
</button>
</form>
);
}
```
Creates a type-safe server action wrapper.
```typescript
export const myAction = createAction<MyDataType>(
async (data: MyDataType) => {
// Your action logic here
// Throw error() or success() to return state
success("Done!");
},
"myActionName", // Optional context for logging
);
```
**Parameters:**
- `handler` - Async function that receives typed data and returns/throws ActionState
- `context` - Optional string for error logging context
Hook to use a server action in your component.
```typescript
const { dispatch, state, isPending } = useAction<MyDataType>(myAction, {
onSuccess: (state) => {
/* called when success() is thrown */
},
onError: (state) => {
/* called when error() is thrown */
},
});
// Call with data
dispatch(myData);
```
**Returns:**
- `dispatch` - Function to call the action with data
- `state` - Current ActionState (undefined until first call)
- `isPending` - Boolean indicating if action is running
### `success(message?, options?)`
Throw in your action to return success state.
```typescript
success("Operation completed!", {
redirect: "/next-page",
refresh: true,
});
```
Throw in your action to return error state.
```typescript
error("Something went wrong", {
redirect: "/error",
});
```
The return type of actions is a **discriminated union**:
```typescript
type ActionState =
| { success: true; message?: string; redirect?: string; refresh?: boolean }
| { success: false; message?: string; redirect?: string; refresh?: boolean };
```
You can safely narrow the type:
```typescript
if (state?.success) {
// state is success state here
console.log("Success:", state.message);
} else if (state?.success === false) {
// state is error state here
console.log("Error:", state.message);
}
```
- Next.js 16+ (App Router)
- React 19+
- TypeScript 5+
Contributions are welcome! Please feel free to submit a Pull Request.
MIT © [Mattia Malonni](https://github.com/mattiamalonni)
- [GitHub Repository](https://github.com/mattiamalonni/next-form-action)
- [Issues](https://github.com/mattiamalonni/next-form-action/issues)
- [npm Package](https://www.npmjs.com/package/next-form-action)