第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 的三个层次

  1. 正确区分 Server/Client 边界(最小化 ‘use client’ 范围):只有确实需要交互的最小部分才是 Client Component,数据获取和展示留在 Server
  2. 组合模式(Server 传数据给 Client):Server Component 获取数据,通过 props 传给 Client Component 处理交互,两者可以混合
  3. Server Actions(替代 API 路由):表单提交和数据变更直接用 Server Actions,不需要写单独的 POST API 路由

→ 第4章:AI帮我写了表单但没有用Actions和useActionState