表單與資料異動
表單讓您能在網頁應用程式中建立與更新資料。Next.js 提供了使用 伺服器動作 (Server Actions) 來處理表單提交與資料異動的強大方式。
範例
伺服器動作如何運作
使用伺服器動作時,您無需手動建立 API 端點。相反地,您可以定義能直接從元件呼叫的非同步伺服器函式。
🎥 觀看: 透過應用程式路由了解更多關於表單與資料異動的內容 → YouTube (10 分鐘)。
伺服器動作可在伺服器元件中定義或從客戶端元件呼叫。在伺服器元件中定義動作能讓表單在無需 JavaScript 的情況下運作,實現漸進增強。
在 next.config.js
檔案中啟用伺服器動作:
module.exports = {
experimental: {
serverActions: true,
},
}
須知事項:
- 從伺服器元件呼叫伺服器動作的表單可在無 JavaScript 的情況下運作。
- 從客戶端元件呼叫伺服器動作的表單會在 JavaScript 尚未載入時排隊提交,優先處理客戶端水合。
- 伺服器動作繼承使用它們的頁面或佈局的 執行環境 (runtime)。
- 伺服器動作可與完全靜態的路由搭配使用(包括使用 ISR 重新驗證資料)。
重新驗證快取資料
伺服器動作與 Next.js 的 快取與重新驗證 架構深度整合。當表單提交時,伺服器動作可更新快取資料並重新驗證應變更的快取鍵。
與傳統應用程式受限於每個路由僅能有一個表單不同,伺服器動作讓每個路由能擁有多個動作。此外,瀏覽器在表單提交時無需重新整理。在單次網路往返中,Next.js 能同時回傳更新後的 UI 與重新整理的資料。
查看以下範例以了解 從伺服器動作重新驗證資料。
範例
僅伺服器表單
要建立僅伺服器表單,請在伺服器元件中定義伺服器動作。動作可透過在函式頂部使用 "use server"
指令內聯定義,或在檔案頂部使用指令的獨立檔案中定義。
export default function Page() {
async function create(formData: FormData) {
'use server'
// 異動資料
// 重新驗證快取
}
return <form action={create}>...</form>
}
export default function Page() {
async function create(formData) {
'use server'
// 異動資料
// 重新驗證快取
}
return <form action={create}>...</form>
}
須知事項:
<form action={create}>
接受 FormData 資料類型。在上例中,透過 HTMLform
提交的 FormData 可在伺服器動作create
中存取。
重新驗證資料
伺服器動作讓您能按需使 Next.js 快取 失效。您可以使用 revalidatePath
使整個路由區段失效:
'use server'
import { revalidatePath } from 'next/cache'
export default async function submit() {
await submitForm()
revalidatePath('/')
}
'use server'
import { revalidatePath } from 'next/cache'
export default async function submit() {
await submitForm()
revalidatePath('/')
}
或使用 revalidateTag
透過快取標籤使特定資料獲取失效:
'use server'
import { revalidateTag } from 'next/cache'
export default async function submit() {
await addPost()
revalidateTag('posts')
}
'use server'
import { revalidateTag } from 'next/cache'
export default async function submit() {
await addPost()
revalidateTag('posts')
}
重新導向
若您想在伺服器動作完成後將使用者重新導向至不同路由,您可以使用 redirect
與任何絕對或相對 URL:
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export default async function submit() {
const id = await addPost()
revalidateTag('posts') // 更新快取文章
redirect(`/post/${id}`) // 導航至新路由
}
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export default async function submit() {
const id = await addPost()
revalidateTag('posts') // 更新快取文章
redirect(`/post/${id}`) // 導航至新路由
}
表單驗證
我們建議使用 HTML 驗證如 required
和 type="email"
來進行基本表單驗證。
對於更進階的伺服器端驗證,可使用架構驗證函式庫如 zod 來驗證解析後表單資料的結構:
import { z } from 'zod'
const schema = z.object({
// ...
})
export default async function submit(formData: FormData) {
const parsed = schema.parse({
id: formData.get('id'),
})
// ...
}
import { z } from 'zod'
const schema = z.object({
// ...
})
export default async function submit(formData) {
const parsed = schema.parse({
id: formData.get('id'),
})
// ...
}
顯示載入狀態
使用 useFormStatus
鉤子來顯示表單在伺服器提交時的載入狀態。useFormStatus
鉤子僅能作為使用伺服器動作的 form
元素的子元素使用。
例如,以下提交按鈕:
<SubmitButton />
接著可用於帶有伺服器動作的表單中:
import { SubmitButton } from '@/app/submit-button'
export default async function Home() {
return (
<form action={...}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
import { SubmitButton } from '@/app/submit-button'
export default async function Home() {
return (
<form action={...}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
錯誤處理
伺服器動作 (Server Actions) 也可以回傳可序列化物件。例如,您的伺服器動作可以處理建立新項目時的錯誤:
'use server'
export async function createTodo(prevState: any, formData: FormData) {
try {
await createItem(formData.get('todo'))
return revalidatePath('/')
} catch (e) {
return { message: 'Failed to create' }
}
}
'use server'
export async function createTodo(prevState, formData) {
try {
await createItem(formData.get('todo'))
return revalidatePath('/')
} catch (e) {
return { message: 'Failed to create' }
}
}
然後,在客戶端元件 (Client Component) 中,您可以讀取這個值並顯示錯誤訊息。
'use client'
import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'
const initialState = {
message: null,
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}
export function AddForm() {
const [state, formAction] = useFormState(createTodo, initialState)
return (
<form action={formAction}>
<label htmlFor="todo">Enter Task</label>
<input type="text" id="todo" name="todo" required />
<SubmitButton />
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
</form>
)
}
'use client'
import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'
const initialState = {
message: null,
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}
export function AddForm() {
const [state, formAction] = useFormState(createTodo, initialState)
return (
<form action={formAction}>
<label htmlFor="todo">Enter Task</label>
<input type="text" id="todo" name="todo" required />
<SubmitButton />
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
</form>
)
}
樂觀更新 (Optimistic Updates)
使用 useOptimistic
在伺服器動作完成前樂觀地更新 UI,而不需等待回應:
'use client'
import { experimental_useOptimistic as 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>
)
}
'use client'
import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './actions'
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage }]
)
return (
<div>
{optimisticMessages.map((m) => (
<div>{m.message}</div>
))}
<form
action={async (formData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}}
>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}
設定 Cookie
您可以在伺服器動作中使用 cookies
函式設定 cookie:
'use server'
import { cookies } from 'next/headers'
export async function create() {
const cart = await createCart()
cookies().set('cartId', cart.id)
}
'use server'
import { cookies } from 'next/headers'
export async function create() {
const cart = await createCart()
cookies().set('cartId', cart.id)
}
讀取 Cookie
您可以在伺服器動作中使用 cookies
函式讀取 cookie:
'use server'
import { cookies } from 'next/headers'
export async function read() {
const auth = cookies().get('authorization')?.value
// ...
}
'use server'
import { cookies } from 'next/headers'
export async function read() {
const auth = cookies().get('authorization')?.value
// ...
}
刪除 Cookie
您可以在伺服器動作中使用 cookies
函式刪除 cookie:
'use server'
import { cookies } from 'next/headers'
export async function delete() {
cookies().delete('name')
// ...
}
'use server'
import { cookies } from 'next/headers'
export async function delete() {
cookies().delete('name')
// ...
}
查看其他範例了解如何從伺服器動作刪除 cookie。