重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
npx skills add https://github.com/next-safe-action/skills --skill safe-action-client// src/lib/safe-action.ts
import { createSafeActionClient } from "next-safe-action";
export const actionClient = createSafeActionClient();
// src/app/actions.ts
"use server";
import { z } from "zod";
import { actionClient } from "@/lib/safe-action";
export const greetUser = actionClient
.inputSchema(z.object({ name: z.string().min(1) }))
.action(async ({ parsedInput: { name } }) => {
return { greeting: `Hello, ${name}!` };
});
createSafeActionClient(opts?)
.use(middleware) // 可重复,向链中添加中间件
.metadata(data) // 如果设置了 defineMetadataSchema 则为必需
.inputSchema(schema, utils?) // 标准模式或异步工厂函数
.bindArgsSchemas([...]) // .bind() 参数的模式(与 inputSchema 的顺序可灵活调整)
.outputSchema(schema) // 验证操作返回值
.action(serverCodeFn, utils?) // 创建 SafeActionFn
.stateAction(serverCodeFn, utils?) // 创建 SafeStateActionFn(用于 useActionState)
每个方法都返回一个新的客户端实例——该链是不可变的。
| 入口点 |
|---|
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 环境 |
|---|
| 导出内容 |
|---|
next-safe-action | 服务器 | createSafeActionClient, createMiddleware, returnValidationErrors, flattenValidationErrors, formatValidationErrors, DEFAULT_SERVER_ERROR_MESSAGE, 错误类,所有核心类型 |
next-safe-action/hooks | 客户端 | useAction, useOptimisticAction, 钩子类型 |
next-safe-action/stateful-hooks | 客户端 | useStateAction(已弃用——请直接使用 React 的 useActionState) |
// 错误:缺少 "use server" 指令——操作将无法工作
import { actionClient } from "@/lib/safe-action";
export const myAction = actionClient.action(async () => {});
// 正确:始终在操作文件中包含 "use server"
"use server";
import { actionClient } from "@/lib/safe-action";
export const myAction = actionClient.action(async () => {});
// 错误:当定义了 metadataSchema 时,调用 .action() 而没有调用 .metadata()
const client = createSafeActionClient({
defineMetadataSchema: () => z.object({ actionName: z.string() }),
});
client.action(async () => {}); // TypeScript 错误!
// 正确:当定义了模式时,始终在 .action() 之前提供元数据
client
.metadata({ actionName: "myAction" })
.action(async () => {});
// 错误:返回错误而不是抛出错误
export const myAction = actionClient
.inputSchema(z.object({ email: z.string().email() }))
.action(async ({ parsedInput }) => {
const exists = await db.user.findByEmail(parsedInput.email);
if (exists) {
return { error: "Email taken" }; // 非类型安全,非标准化
}
});
// 正确:使用 returnValidationErrors 处理字段级错误
import { returnValidationErrors } from "next-safe-action";
export const myAction = actionClient
.inputSchema(z.object({ email: z.string().email() }))
.action(async ({ parsedInput }) => {
const exists = await db.user.findByEmail(parsedInput.email);
if (exists) {
returnValidationErrors(z.object({ email: z.string().email() }), {
email: { _errors: ["Email is already in use"] },
});
}
return { success: true };
});
传递给 .action() 的函数接收一个对象:
.action(async ({
parsedInput, // 已验证的输入(根据 inputSchema 进行类型推断)
clientInput, // 原始的客户端输入(unknown 类型)
bindArgsParsedInputs, // 已验证的绑定参数元组
bindArgsClientInputs, // 原始的绑定参数
ctx, // 来自中间件链的上下文
metadata, // 通过 .metadata() 设置的元数据
}) => {
// 返回数据
});
对于 .stateAction(),会添加第二个参数:
.stateAction(async ({ parsedInput, ctx }, { prevResult }) => {
// prevResult 是之前的 SafeActionResult(经过 structuredClone 处理)
return { count: (prevResult.data?.count ?? 0) + 1 };
});
每周安装量
64
代码仓库
首次出现
2026年3月6日
安全审计
安装于
cursor63
gemini-cli63
amp63
github-copilot63
codex63
kimi-cli63
// src/lib/safe-action.ts
import { createSafeActionClient } from "next-safe-action";
export const actionClient = createSafeActionClient();
// src/app/actions.ts
"use server";
import { z } from "zod";
import { actionClient } from "@/lib/safe-action";
export const greetUser = actionClient
.inputSchema(z.object({ name: z.string().min(1) }))
.action(async ({ parsedInput: { name } }) => {
return { greeting: `Hello, ${name}!` };
});
createSafeActionClient(opts?)
.use(middleware) // repeatable, adds middleware to chain
.metadata(data) // required if defineMetadataSchema is set
.inputSchema(schema, utils?) // Standard Schema or async factory function
.bindArgsSchemas([...]) // schemas for .bind() arguments (order with inputSchema is flexible)
.outputSchema(schema) // validates action return value
.action(serverCodeFn, utils?) // creates SafeActionFn
.stateAction(serverCodeFn, utils?) // creates SafeStateActionFn (for useActionState)
Each method returns a new client instance — the chain is immutable.
| Entry point | Environment | Exports |
|---|---|---|
next-safe-action | Server | createSafeActionClient, createMiddleware, returnValidationErrors, flattenValidationErrors, formatValidationErrors, DEFAULT_SERVER_ERROR_MESSAGE, error classes, all core types |
next-safe-action/hooks |
// BAD: Missing "use server" directive — action won't work
import { actionClient } from "@/lib/safe-action";
export const myAction = actionClient.action(async () => {});
// GOOD: Always include "use server" in action files
"use server";
import { actionClient } from "@/lib/safe-action";
export const myAction = actionClient.action(async () => {});
// BAD: Calling .action() without .metadata() when metadataSchema is defined
const client = createSafeActionClient({
defineMetadataSchema: () => z.object({ actionName: z.string() }),
});
client.action(async () => {}); // TypeScript error!
// GOOD: Always provide metadata before .action() when schema is defined
client
.metadata({ actionName: "myAction" })
.action(async () => {});
// BAD: Returning an error instead of throwing
export const myAction = actionClient
.inputSchema(z.object({ email: z.string().email() }))
.action(async ({ parsedInput }) => {
const exists = await db.user.findByEmail(parsedInput.email);
if (exists) {
return { error: "Email taken" }; // Not type-safe, not standardized
}
});
// GOOD: Use returnValidationErrors for field-level errors
import { returnValidationErrors } from "next-safe-action";
export const myAction = actionClient
.inputSchema(z.object({ email: z.string().email() }))
.action(async ({ parsedInput }) => {
const exists = await db.user.findByEmail(parsedInput.email);
if (exists) {
returnValidationErrors(z.object({ email: z.string().email() }), {
email: { _errors: ["Email is already in use"] },
});
}
return { success: true };
});
The function passed to .action() receives a single object:
.action(async ({
parsedInput, // validated input (typed from inputSchema)
clientInput, // raw client input (unknown)
bindArgsParsedInputs, // validated bind args tuple
bindArgsClientInputs, // raw bind args
ctx, // context from middleware chain
metadata, // metadata set via .metadata()
}) => {
// return data
});
For .stateAction(), a second argument is added:
.stateAction(async ({ parsedInput, ctx }, { prevResult }) => {
// prevResult is the previous SafeActionResult (structuredClone'd)
return { count: (prevResult.data?.count ?? 0) + 1 };
});
Weekly Installs
64
Repository
First Seen
Mar 6, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
cursor63
gemini-cli63
amp63
github-copilot63
codex63
kimi-cli63
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
122,000 周安装
| Client |
useAction, useOptimisticAction, hook types |
next-safe-action/stateful-hooks | Client | useStateAction (deprecated — use React's useActionState directly) |