Server Actions and Mutations
Server Actions是在服务器上执行的异步函数。 它们可以在Next.js应用程序的Server和Client Components中用于处理表单提交和数据变更。
🎥 观看:通过Server Actions学习有关表单和变更的更多信息 → YouTube(10分钟)。
约定
可以使用React的"use server"
指令定义Server Action。
您可以将该指令放在异步函数的顶部,以将该函数标记为Server Action,
或者将其放在单独的文件的顶部,以将该文件的所有导出标记为Server Action。
Server Components
Server Components可以使用内联函数级别或模块级别的"use server"
指令。
要内联Server Action,请在函数体的顶部添加"use server"
:
// Server Component
export default function Page() {
// Server Action
async function create() {
'use server'
// ...
}
return (
// ...
)
}
Client Components
Client Components只能导入使用模块级别"use server"
指令的操作。
要在Client Component中调用Server Action,请创建一个新文件,并在其顶部添加"use server"
指令。
文件中的所有函数都将被标记为Server Actions,可以在Client和Server Components中重复使用:
'use server'
export async function create() {
// ...
}
import { create } from '@/app/actions'
export function Button() {
return (
// ...
)
}
您还可以将Server Action作为prop传递给Client Component:
<ClientComponent updateItem={updateItem} />
'use client'
export default function ClientComponent({ updateItem }) {
return <form action={updateItem}>{/* ... */}</form>
}
行为
- Server actions可以使用
<form>
元素的action
属性调用:- Server Components默认支持渐进增强,这意味着即使JavaScript尚未加载或被禁用,表单也将被提交。
- 在Client Components中,如果JavaScript尚未加载,调用Server Actions的表单将排队提交,优先考虑客户端水合。
- 在水合后,浏览器不会在表单提交时刷新。
- Server Actions不仅限于
<form>
,还可以从事件处理程序、useEffect
、第三方库和其他表单元素(如<button>
)中调用。 - Server Actions 与Next.js的缓存和重新验证架构集成。 当调用一个动作时,Next.js可以在单个服务器往返中返回更新后的UI和新数据。
- 在幕后,动作使用
POST
方法,只有此HTTP方法才能调用它们。 - Server Actions的参数和返回值必须由React可序列化。请参阅React文档以获取 可序列化参数和值的列表。
- Server Actions是函数。这意味着它们可以在应用程序的任何地方重复使用。
- Server Actions继承它们所用页面或布局的 运行时。
示例
表单
React扩展了HTML的<form>
元素,以允许使用action
prop调用Server Actions。
在表单中调用时,action会自动接收
FormData对象。
您无需使用React的useState
来管理字段,而是可以使用本机的
FormData方法提取数据:
export default function Page() {
async function createInvoice(formData: FormData) {
'use server'
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// mutate data
// revalidate cache
}
return <form action={createInvoice}>...</form>
}
需要注意的是:
- 示例:带有加载和错误状态的表单
- 在处理具有许多字段的表单时,您可能希望考虑使用JavaScript的
Object.fromEntries()
方法。 例如:const rawFormData = Object.fromEntries(formData.entries())
- 有关更多信息,请参阅React
<form>
文档。
传递额外参数
您可以使用JavaScript的bind
方法将额外参数传递给Server Action。
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update User Name</button>
</form>
)
}
Server Action将接收userId
参数,除了表单数据:
'use server'
export async function updateUser(userId, formData) {
// ...
}
需要注意的是:
- 作为替代方案,可以在表单中将参数作为隐藏的输入字段传递(例如
<input type="hidden" name="userId" value={userId} />
)。 但是,该值将成为渲染的HTML的一部分,不会被编码。 .bind
在Server和Client Components中都可以工作。它还支持渐进增强。
待定状态
您可以使用React的useFormStatus hook在表单提交时显示待定状态。
useFormStatus
为特定的<form>
返回状态,因此必须将其定义为<form>
元素的子元素。useFormStatus
是一个React hook,因此必须在Client Component中使用。
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}
<SubmitButton />
然后可以嵌套在任何表单中:
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
// Server Component
export default async function Home() {
return (
<form action={createItem}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
服务器端验证和错误处理
我们建议使用HTML验证,如required
和type="email"
进行基本的客户端表单验证。
对于更高级的服务器端验证,您可以使用 zod 等库在变异数据之前验证表单字段:
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Invalid Email',
}),
})
export default async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// 如果表单数据无效,请提前返回
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Mutate data
}
一旦在服务器上验证了字段,您就可以在操作中返回可序列化的对象, 并使用React的useFormState hook来向用户显示消息。
- 通过将操作传递给
useFormState
,操作的函数签名会更改, 以接收新的prevState
或initialState
参数作为其第一个参数。 useFormState
是一个 React hook,因此必须在客户端组件中使用。
'use server'
export async function createUser(prevState: any, formData: FormData) {
// ...
return {
message: 'Please enter a valid email',
}
}
然后,您可以将操作传递给useFormState
hook,并使用返回的状态来显示错误消息。
'use client'
import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'
const initialState = {
message: null,
}
export function Signup() {
const [state, formAction] = useFormState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button>Sign up</button>
</form>
)
}
需要注意的是:
- 在变异数据之前,您应始终确保用户还被授权执行该操作。请参阅 身份验证和授权。
乐观更新
您可以使用React的useOptimistic
hook在Server Action完成之前乐观地更新UI,而不是等待响应:
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
type Message = {
message: string
}
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[]>(
messages,
(state: Message[], newMessage: string) => [
...state,
{ message: newMessage },
]
)
return (
<div>
{optimisticMessages.map((m, k) => (
<div key={k}>{m.message}</div>
))}
<form
action={async (formData: FormData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}}
>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}
嵌套元素
您可以在嵌套在<form>
内的元素中调用Server Action,
例如<button>
、<input type="submit">
和<input type="image">
。
这些元素接受formAction
prop或事件处理程序。
这在您想要在一个表单中调用多个服务器操作的情况下很有用。
例如,您可以为保存文章草稿和发布文章分别创建一个特定的<button>
元素。
有关更多信息,请参阅React <form>
文档。
非表单元素
虽然在<form>
元素中使用Server Actions很常见,但它们也可以从代码的其他部分调用,例如事件处理程序和useEffect
。
事件处理程序
您可以从onClick
等事件处理程序中调用Server Action,例如增加喜欢次数:
'use client'
import { incrementLike } from './actions'
import { useState } from 'react'
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)
return (
<>
<p>Total Likes: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}
>
Like
</button>
</>
)
}
为了提高用户体验,我们建议使用其他React API,
如useOptimistic
和
useTransition
,
在服务器操作在服务器上执行之前更新UI,或者在展示待定状态时。
您还可以向表单元素添加事件 处理程序,例如在onChange
时保存表单字段:
'use client'
import { publishPost, saveDraft } from './actions'
export default function EditPost() {
return (
<form action={publishPost}>
<textarea
name="content"
onChange={async (e) => {
await saveDraft(e.target.value)
}}
/>
<button type="submit">Publish</button>
</form>
)
}
对于像这样可能快速触发多个事件的情况,我们建议使用防抖以防止不必要的Server Action调用。
useEffect
您可以使用React 的useEffect
hook在组件挂载时或依赖项更改时调用Server Action。
这对于依赖于全局事件或需要自动触发的变异很有用。
例如,用于应用程序快捷键的onKeyDown
,用于无限滚动的交叉观察器hook,或在组件挂载时更新查看计数:
'use client'
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
}
updateViews()
}, [])
return <p>Total Views: {views}</p>
}
请记住考虑 useEffect
的行为和注意事项。
错误处理
当发生错误时,它将被客户端上最近的error.js
或<Suspense>
边界捕获。
我们建议使用try/catch
将错误返回以由您的UI处理。
例如,您的Server Action可能会通过返回消息来处理创建新项目时的错误:
'use server'
export async function createTodo(prevState: any, formData: FormData) {
try {
// Mutate data
} catch (e) {
throw new Error('Failed to create task')
}
}
值得知道:
- 除了抛出错误之外,您还可以返回一个对象,由
useFormStatus
处理。请参阅服务器端验证和错误处理。
重新验证数据
您可以在Server Actions中使用revalidatePath
API重新验证
Next.js缓存:
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidatePath('/posts')
}
或者使用revalidateTag
使用缓存标签使特定数据获取失效:
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts')
}
重定向
如果希望在完成Server Action后将用户重定向到不同的路由,
则可以使用redirect
API。redirect
需要在try/catch
块之外调用:
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function createPost(id: string) {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts') // 更新缓存的文章
redirect(`/post/${id}`) // 转到新的文章页面
}
Cookies
您可以在Server Action中使用cookies
API的get
、set
和delete
Cookie:
'use server'
import { cookies } from 'next/headers'
export async function exampleAction() {
// 获取Cookie
const value = cookies().get('name')?.value
// 设置Cookie
cookies().set('name', 'Delba')
// 删除Cookie
cookies().delete('name')
}
请查看从Server Actions删除Cookie的 其他示例。
安全性
身份验证和授权
您应该将Server Actions视为面向公共API端点的方式,并确保用户被授权执行操作。例如:
'use server'
import { auth } from './lib'
export function addItem() {
const { user } = auth()
if (!user) {
throw new Error('You must be signed in to perform this action')
}
// ...
}
闭包和加密
在组件内定义Server Action会创建一个闭包,
其中该动作可以访问外部函数范围的变量。
例如,publish
动作可以访问publishVersion
变量:
export default function Page() {
const publishVersion = await getLatestVersion();
async function publish(formData: FormData) {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('The version has changed since pressing publish');
}
...
}
return <button action={publish}>Publish</button>;
}
当您需要捕获数据快照(例如publishVersion
)以便在调用动作时稍后使用时,闭包很有用。
然而,为了发生这种情况,捕获的变量在动作被调用时被发送到客户端,并在动作被调用时返回服务器。 为防止将敏感数据暴露给客户端,Next.js会自动加密封闭的变量。 每次构建Next.js应用程序时,都会为每个动作生成一个新的私钥。这意味着动作只能为特定构建调用。
我们不建议仅依赖加密防止将敏感值暴露给客户端。 相反,您应该使用React污染API 主动阻止将特定数据发送到客户端。