第03章:AI不知道什么时候用Server Components
第03章:AI不知道什么时候用Server Components
“你在用 Next.js 15,让 AI 写了一个数据展示组件。AI 给你加了 useState、useEffect、‘use client’——一个完全正常的客户端组件。但这个组件只是展示数据,不需要任何交互。它本来可以是一个 Server Component:直接在服务器读数据库,没有 JavaScript bundle 发送到客户端,更快的首屏渲染,更好的 SEO。AI 之所以不用 Server Components,是因为它不确定什么时候能用、什么时候不能用。”
ℹ️ 版本说明:本章基于 React 19.2.6 + Next.js 15.x。
3.1 AI默认会生成什么
// AI 通常给你的代码(把所有东西都做成客户端组件)
'use client';
import { useState, useEffect } from 'react';
export default function BlogPosts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
3.2 AI通常遗漏的4个坑
⚠️ 坑1:不知道 Next.js App Router 里默认是 Server Component
// Next.js 15 App Router:默认所有组件都是 Server Component
// 只有显式加 'use client' 才是 Client Component
// ✅ Server Component(默认,不需要 'use client')
// app/blog/page.tsx
export default async function BlogPage() {
// 可以直接查数据库!不需要 API 路由
const posts = await db.query('SELECT * FROM posts ORDER BY created_at DESC');
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// 效果:0 JavaScript 发送到客户端,直接 HTML,SEO 友好
⚠️ 坑2:Server Component 和 Client Component 的边界
// Server Component 的限制(不能做这些):
// ❌ 不能用 useState, useEffect 等 hooks
// ❌ 不能监听 onClick, onChange 等事件
// ❌ 不能访问浏览器 API(window, localStorage)
// ❌ 不能用 Context(useContext)
// Client Component 的限制(不能做这些):
// ❌ 不能直接查数据库
// ❌ 不能读取 Server 环境变量(process.env.SECRET)
// ❌ 不能用 cookies()、headers() 等 Next.js Server API
// 最佳实践:把应用分为两层
// ┌─────────────────────────────────────────┐
// │ Server Component(数据、布局、SEO内容) │
// │ ├── fetch data from DB │
// │ ├── pass data as props ↓ │
// │ └─── Client Component(只处理交互) │
// │ ├── useState / useEffect │
// │ └── event handlers │
// └─────────────────────────────────────────┘
// 示例:Server Component 传数据给 Client Component
// app/posts/[id]/page.tsx(Server Component)
export default async function PostPage({ params }) {
const post = await db.getPost(params.id);
const comments = await db.getComments(params.id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* LikeButton 需要交互,是 Client Component */}
<LikeButton postId={post.id} initialLikes={post.likes} />
{/* CommentList 只展示,是 Server Component */}
<CommentList comments={comments} />
</article>
);
}
⚠️ 坑3:Context 在 Server Component 里不工作
// 问题:很多 AI 会这么写,但在 App Router 里有问题
'use client';
const ThemeContext = createContext('light');
// Server Component 无法 useContext!
// ❌ 在 Server Component 里:
const theme = useContext(ThemeContext); // 错误!
// 正确方案A:Context Provider 包裹 Client Component 边界
// providers.tsx
'use client';
export function Providers({ children, theme }) {
return (
<ThemeContext.Provider value={theme}>
{children} {/* children 可以是 Server Components! */}
</ThemeContext.Provider>
);
}
// layout.tsx(Server Component)
export default async function Layout({ children }) {
const theme = await getUserTheme(); // 服务器读取
return (
<Providers theme={theme}>
{children} {/* children 仍然是 Server Components */}
</Providers>
);
}
⚠️ 坑4:误以为 Server Component 和 Client Component 不能混合
// 误解:Server Component 里不能有 Client Component
// 正确:Server Component 可以 import Client Component
// Server Component(数据层)
import LikeButton from './LikeButton'; // Client Component
export default async function Post({ postId }) {
const post = await db.getPost(postId); // 服务器端数据获取
return (
<div>
<h1>{post.title}</h1>
{/* Server Component 可以传 props 给 Client Component */}
<LikeButton postId={postId} likes={post.likes} />
</div>
);
}
// LikeButton.tsx(Client Component)
'use client';
export function LikeButton({ postId, likes }) {
const [count, setCount] = useState(likes);
return (
<button onClick={() => setCount(c => c + 1)}>
❤️ {count}
</button>
);
}
// 重点:Client Component 不能 import Server Component!
// ❌ 这样会出错:
'use client';
import ServerComponent from './ServerComponent'; // 错误!Client 不能 import Server
// ✅ 但 Server Component 可以作为 children 传进来
3.3 更好的提示词
提示词 P01:判断组件应该是 Server 还是 Client
使用时机:写新组件时,不确定用哪种
帮我决定 React 19.2.6 + Next.js 15 App Router 里,这个组件应该是 Server Component 还是 Client Component。
组件需求:
[描述组件的功能和用途]
判断标准帮我分析:
1. 是否需要 useState / useEffect?(需要 → Client)
2. 是否有 onClick / onChange 等事件处理?(有 → Client)
3. 是否需要访问 localStorage / window?(需要 → Client)
4. 是否只是展示数据?(只展示 → Server)
5. 是否需要直接访问数据库/文件?(需要 → Server)
如果应该是 Client Component,帮我识别最小化的 Client 边界:
- 哪些部分必须是 Client(有交互)
- 哪些部分可以是 Server(只展示)
- 如何拆分,让 Client bundle 尽量小
给我拆分方案和完整代码。
基于 React 19.2.6 + Next.js 15 App Router。
提示词 P02:把 Client Component 重构为 Server + Client 组合
使用时机:你有一个全是 ‘use client’ 的页面,想优化为 Server Component
帮我把这个 Next.js 15 Client Component 重构为 Server + Client 组合。
当前代码(全是 'use client'):
[粘贴组件代码]
目标:
1. 数据获取部分移到 Server Component(直接查数据库或调 API)
2. 交互部分保留 Client Component
3. 通过 props 传递数据
重构后需要:
- Server Component 直接 async 获取数据
- Client Component 只接收已准备好的数据,处理交互
- 如果有 Context,帮我找到正确的 Provider 位置
预期效果:
- 减少 JavaScript bundle 大小(哪些 JS 可以移出客户端?)
- 改善首屏渲染速度(哪些内容可以服务器渲染?)
基于 React 19.2.6 + Next.js 15 App Router。
提示词 P03:Server Actions(服务器端表单处理)
使用时机:表单提交、数据变更操作,不想写 API 路由
帮我用 React 19.2.6 + Next.js 15 的 Server Actions 实现表单提交。
表单功能:[创建博客文章 / 更新用户资料 / 删除数据]
Server Actions 的优势:
- 不需要写 /api/xxx 路由
- 直接在 Server Component 里写服务器逻辑
- 自动 CSRF 保护
实现要求:
1. Server Action 函数('use server' 标记):
- 接收 FormData(如果是 form action)或参数(如果是按钮点击)
- 直接操作数据库
- 用 revalidatePath / revalidateTag 更新缓存
2. 在 form 里使用:
<form action={createPost}> {/* Server Action 作为 form action */}
3. 在按钮里使用(用 useActionState):
const [state, action, isPending] = useActionState(deletePost, null);
4. 错误处理和验证(Zod schema)
5. 成功后重定向:redirect('/posts')
基于 React 19.2.6 + Next.js 15 App Router。
3.4 验收清单
| 检查项 | 验证方法 | AI辅助 |
|---|---|---|
| 纯展示组件无 ‘use client’ | 搜索组件文件,无 ‘use client’ 标记 | 用 P02 重构 |
| 数据获取在 Server Component | 无 fetch + useEffect 的数据获取 | 用 P02 重构 |
| Context Provider 位置正确 | Server Component 的 children 能正常渲染 | 用 P01 分析 |
| 表单提交用 Server Actions | 无对应的 /api/ POST 路由 | 用 P03 改造 |
| Client bundle 大小减小 | Next.js build 输出里 First Load JS 减小 | - |
| 无 Client Component import Server Component | 搜索 ‘use client’ 文件里的 import | 用 P01 检查 |
3.5 本章小结
如果你只记一件事:在 Next.js 15 App Router 里,默认不写 'use client'——只有当组件需要交互(useState、事件处理、浏览器 API)时才加。纯展示组件保持为 Server Component,可以直接读数据库,减少客户端 JavaScript 体积,首屏渲染更快。
Server Components 的三个层次:
- 正确区分 Server/Client 边界(最小化 ‘use client’ 范围):只有确实需要交互的最小部分才是 Client Component,数据获取和展示留在 Server
- 组合模式(Server 传数据给 Client):Server Component 获取数据,通过 props 传给 Client Component 处理交互,两者可以混合
- Server Actions(替代 API 路由):表单提交和数据变更直接用 Server Actions,不需要写单独的 POST API 路由
→ 第4章:AI帮我写了表单但没有用Actions和useActionState