主题
08 · 表单与 Server Actions
目标:用 Server Actions 完成「列表 + 新增 / 删除」写操作闭环,不必先搭一套 REST 客户端。
官方主文:Server Actions and Mutations · Forms(路径以侧栏为准)
1. Server Action 是什么
在服务端执行的异步函数,可从 Client / Server 组件的 <form action={fn}> 调用,适合变更数据(增删改),然后 revalidatePath / revalidateTag 刷新界面。
tsx
'use server'
export async function createNote(formData: FormData) {
// 服务端:校验、写存储、revalidate
}1
2
3
4
5
2
3
4
5
对照:接近「Koa 的 POST handler」,但绑定在 React 表单模型上,少写一层手写 fetch('/api')(Route Handlers 仍可用,本系列小项目优先 Actions)。
2. 最小内存版(本篇练手;第 10 篇换 JSON 文件)
lib/notes-store.ts(简单模块级内存,dev 热更新会丢,仅实验):
ts
export type Note = { id: string; title: string }
const g = globalThis as unknown as { __notes?: Note[] }
if (!g.__notes) g.__notes = []
export function listNotes() {
return g.__notes!
}
export function addNote(title: string) {
const note = { id: String(Date.now()), title }
g.__notes!.unshift(note)
return note
}
export function removeNote(id: string) {
g.__notes = g.__notes!.filter((n) => n.id !== id)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app/actions/notes.ts
ts
'use server'
import { revalidatePath } from 'next/cache'
import { addNote, removeNote } from '@/lib/notes-store'
export async function createNoteAction(formData: FormData) {
const title = String(formData.get('title') ?? '').trim()
if (!title) return
addNote(title)
revalidatePath('/actions-lab')
}
export async function deleteNoteAction(formData: FormData) {
const id = String(formData.get('id') ?? '')
if (!id) return
removeNote(id)
revalidatePath('/actions-lab')
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app/actions-lab/page.tsx
tsx
import { createNoteAction, deleteNoteAction } from '@/app/actions/notes'
import { listNotes } from '@/lib/notes-store'
export default function ActionsLabPage() {
const notes = listNotes()
return (
<main style={{ padding: 24, maxWidth: 480 }}>
<h1>Actions Lab</h1>
<form action={createNoteAction}>
<input name="title" placeholder="标题" required />
<button type="submit">新增</button>
</form>
<ul>
{notes.map((n) => (
<li key={n.id}>
{n.title}{' '}
<form action={deleteNoteAction} style={{ display: 'inline' }}>
<input type="hidden" name="id" value={n.id} />
<button type="submit">删</button>
</form>
</li>
))}
</ul>
</main>
)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
访问 /actions-lab:新增 → 列表更新;删除 → 消失。无需 'use client'(渐进增强表单)。
3. 校验与安全(最低线)
- 永远在 Server Action 内再校验一次(长度、空值);不要只信客户端。
- 不要把密钥打进 Client 包。
- 生产环境还要考虑鉴权、CSRF 等——官方 Mutations 文有安全小节;小项目可后置,但心中有数。
4. 何时再引入客户端状态库
| 阶段 | 建议 |
|---|---|
| 本系列 / 第 10 篇 | URL + Server 数据 + Actions 足够 |
| 复杂跨页客户端缓存、乐观更新大面积 | 再评估 Zustand / React Query 等 |
| 「因为课上有 Redux」 | 不够理由;先交付笔记板 |
5. 必做验收
- [ ]
/actions-lab能新增、删除(内存即可) - [ ] Action 文件有
'use server';变更后revalidatePath - [ ] 读过官方 Server Actions 文的 Forms / revalidate 段落
- [ ] 能对比:自己写
fetchPOST vsaction={fn}
6. AI 提示词约束(本篇)
text
技术约束:Next.js App Router Server Actions;
不要引入 tRPC / React Query / Redux。
实现「新增+删除」最小闭环,变更后 revalidatePath。
每个 API 注明 nextjs.org 文档标题。1
2
3
4
2
3
4
7. 下一步
→ 09 · 用 AI 写 React/Next:审查清单与轻量测试
