Skip to content

Server Components与Client Components交互

学习目标

通过本章学习,你将掌握:

  • Server和Client Components的交互规则
  • 数据传递模式
  • 组件组合策略
  • 事件处理方法
  • Server Actions的使用
  • 性能优化技巧
  • 常见问题解决
  • 实战交互模式

第一部分:组合规则

1.1 Server导入Client(✅ 允许)

jsx
// ========== Server Component ==========
// app/page.jsx
import ClientButton from './ClientButton';

async function ServerPage() {
  const data = await fetchData();
  
  return (
    <div>
      <h1>Server Content</h1>
      <p>{data.content}</p>
      
      {/* ✅ Server Component可以渲染Client Component */}
      <ClientButton label="Click Me" />
    </div>
  );
}


// ========== Client Component ==========
// app/ClientButton.jsx
'use client';

export default function ClientButton({ label }) {
  const [clicked, setClicked] = useState(false);
  
  return (
    <button onClick={() => setClicked(true)}>
      {label} {clicked && '✓'}
    </button>
  );
}

1.2 Client不能直接导入Server(❌ 禁止)

jsx
// ❌ 错误的做法
'use client';

import ServerComponent from './ServerComponent';  // 错误!

function ClientComponent() {
  return (
    <div>
      <ServerComponent />  {/* 这会失败 */}
    </div>
  );
}


// ✅ 正确的做法:通过props.children
// Parent.jsx (Server)
import ClientWrapper from './ClientWrapper';
import ServerContent from './ServerContent';

async function Parent() {
  return (
    <ClientWrapper>
      {/* Server Component作为children传递 */}
      <ServerContent />
    </ClientWrapper>
  );
}

// ClientWrapper.jsx (Client)
'use client';

function ClientWrapper({ children }) {
  const [expanded, setExpanded] = useState(false);
  
  return (
    <div>
      <button onClick={() => setExpanded(!expanded)}>
        Toggle
      </button>
      {expanded && children}
    </div>
  );
}

1.3 组件边界

jsx
// 组件树结构及边界

<ServerRoot>                {/* Server */}
  <ServerHeader />          {/* Server */}
  
  <ClientSidebar>           {/* Client - 边界 */}
    <ServerMenu />          {/* ❌ 不能嵌套 */}
  </ClientSidebar>
  
  <ServerMain>              {/* Server */}
    <ClientInteractive />   {/* ✅ 可以嵌套 */}
    <ServerContent>         {/* ✅ Client内的Server通过props */}
      <ClientButton />      {/* ✅ Server可以嵌套Client */}
    </ServerContent>
  </ServerMain>
</ServerRoot>

第二部分:数据传递

2.1 Server向Client传递数据

jsx
// ========== Server Component ==========
async function BlogPost({ id }) {
  // 在服务器获取数据
  const post = await fetchPost(id);
  const comments = await fetchComments(id);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      
      {/* 通过props传递数据给Client Component */}
      <LikeButton 
        postId={post.id}
        initialLikes={post.likes}
      />
      
      <CommentForm postId={post.id} />
      
      <CommentsList 
        comments={comments}
        canModerate={post.author.isCurrentUser}
      />
    </article>
  );
}


// ========== Client Components ==========
'use client';

function LikeButton({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [liked, setLiked] = useState(false);
  
  const handleLike = async () => {
    setLiked(true);
    setLikes(prev => prev + 1);
    
    await fetch(`/api/posts/${postId}/like`, {
      method: 'POST'
    });
  };
  
  return (
    <button onClick={handleLike} disabled={liked}>
      ❤️ {likes}
    </button>
  );
}

2.2 可序列化的Props

jsx
// ✅ 可以传递的数据类型
async function ServerComponent() {
  const data = {
    // ✅ 基本类型
    string: 'hello',
    number: 42,
    boolean: true,
    null: null,
    
    // ✅ 数组
    array: [1, 2, 3],
    
    // ✅ 对象
    object: { key: 'value' },
    
    // ✅ 日期(会被序列化为字符串)
    date: new Date(),
    
    // ✅ 嵌套结构
    nested: {
      deep: {
        value: 'data'
      }
    }
  };
  
  return <ClientComponent data={data} />;
}


// ❌ 不能传递的数据类型
async function BadServerComponent() {
  // ❌ 函数
  const handleClick = () => console.log('click');
  
  // ❌ Class实例
  const instance = new MyClass();
  
  // ❌ Symbol
  const sym = Symbol('key');
  
  // ❌ undefined(会被忽略)
  const undef = undefined;
  
  return (
    <ClientComponent
      onClick={handleClick}  // ❌ 错误!
      instance={instance}    // ❌ 错误!
      symbol={sym}           // ❌ 错误!
    />
  );
}

2.3 通过children传递

jsx
// Server Component
async function DataProvider({ userId }) {
  const user = await fetchUser(userId);
  const permissions = await fetchPermissions(userId);
  
  return (
    <ClientLayout>
      {/* 将Server Component作为children传递 */}
      <ServerSidebar user={user} />
      
      <ServerContent 
        user={user}
        permissions={permissions}
      />
    </ClientLayout>
  );
}

// Client Component
'use client';

function ClientLayout({ children }) {
  const [sidebarOpen, setSidebarOpen] = useState(true);
  
  return (
    <div className="layout">
      <button onClick={() => setSidebarOpen(!sidebarOpen)}>
        Toggle Sidebar
      </button>
      
      <div className={`content ${sidebarOpen ? 'with-sidebar' : ''}`}>
        {children}  {/* 渲染Server Components */}
      </div>
    </div>
  );
}

第三部分:事件处理

3.1 Client Component处理事件

jsx
// Server Component
async function ProductPage({ productId }) {
  const product = await fetchProduct(productId);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>价格: ¥{product.price}</p>
      
      {/* Client Component处理交互 */}
      <AddToCartButton 
        productId={product.id}
        productName={product.name}
        price={product.price}
      />
    </div>
  );
}

// Client Component
'use client';

function AddToCartButton({ productId, productName, price }) {
  const [adding, setAdding] = useState(false);
  const [added, setAdded] = useState(false);
  
  const handleAdd = async () => {
    setAdding(true);
    
    try {
      await fetch('/api/cart', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ productId, quantity: 1 })
      });
      
      setAdded(true);
      setTimeout(() => setAdded(false), 2000);
    } catch (error) {
      alert('添加失败');
    } finally {
      setAdding(false);
    }
  };
  
  return (
    <button 
      onClick={handleAdd}
      disabled={adding}
    >
      {adding ? '添加中...' : added ? '已添加 ✓' : '加入购物车'}
    </button>
  );
}

3.2 使用Server Actions

jsx
// ========== Server Actions ==========
// app/actions.js
'use server';

export async function addToCart(productId, quantity) {
  const session = await getSession();
  
  await db.cart.create({
    data: {
      userId: session.userId,
      productId,
      quantity
    }
  });
  
  revalidatePath('/cart');
  
  return { success: true };
}


// ========== Server Component ==========
import { addToCart } from './actions';

async function ProductPage({ productId }) {
  const product = await fetchProduct(productId);
  
  return (
    <div>
      <h1>{product.name}</h1>
      
      {/* 传递Server Action给Client Component */}
      <AddToCartForm 
        productId={product.id}
        addToCartAction={addToCart}
      />
    </div>
  );
}


// ========== Client Component ==========
'use client';

function AddToCartForm({ productId, addToCartAction }) {
  const [quantity, setQuantity] = useState(1);
  const [pending, setPending] = useState(false);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setPending(true);
    
    try {
      // 调用Server Action
      const result = await addToCartAction(productId, quantity);
      
      if (result.success) {
        alert('已添加到购物车');
      }
    } catch (error) {
      alert('添加失败');
    } finally {
      setPending(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="number"
        value={quantity}
        onChange={(e) => setQuantity(Number(e.target.value))}
        min="1"
      />
      <button type="submit" disabled={pending}>
        {pending ? '添加中...' : '加入购物车'}
      </button>
    </form>
  );
}

3.3 使用useFormStatus

jsx
// Server Action
'use server';

export async function submitContact(formData) {
  const name = formData.get('name');
  const email = formData.get('email');
  const message = formData.get('message');
  
  await db.contact.create({
    data: { name, email, message }
  });
  
  return { success: true };
}

// Server Component
import { submitContact } from './actions';

async function ContactPage() {
  return (
    <div>
      <h1>联系我们</h1>
      <ContactForm action={submitContact} />
    </div>
  );
}

// Client Component
'use client';

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? '发送中...' : '发送'}
    </button>
  );
}

function ContactForm({ action }) {
  return (
    <form action={action}>
      <input name="name" required />
      <input name="email" type="email" required />
      <textarea name="message" required />
      
      <SubmitButton />
    </form>
  );
}

第四部分:Context共享

4.1 Client Context Provider

jsx
// ========== Context Provider (Client) ==========
// app/providers.jsx
'use client';

import { createContext, useState, useContext } from 'react';

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}


// ========== Root Layout (Server) ==========
// app/layout.jsx
import { ThemeProvider } from './providers';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {/* Server Component包裹Client Provider */}
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}


// ========== Client Component使用Context ==========
// app/ThemeToggle.jsx
'use client';

import { useTheme } from './providers';

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      切换到{theme === 'light' ? '深色' : '浅色'}模式
    </button>
  );
}

4.2 混合使用Context

jsx
// Context Provider
'use client';

const UserContext = createContext(null);

export function UserProvider({ initialUser, children }) {
  const [user, setUser] = useState(initialUser);
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

// Root Layout (Server)
async function RootLayout({ children }) {
  // 在服务器获取初始用户数据
  const user = await getCurrentUser();
  
  return (
    <html>
      <body>
        {/* 传递初始数据给Provider */}
        <UserProvider initialUser={user}>
          {children}
        </UserProvider>
      </body>
    </html>
  );
}

// Client Component使用
'use client';

function UserProfile() {
  const { user, setUser } = useContext(UserContext);
  
  const handleLogout = async () => {
    await logout();
    setUser(null);
  };
  
  return (
    <div>
      <p>欢迎, {user.name}</p>
      <button onClick={handleLogout}>退出</button>
    </div>
  );
}

第五部分:实战模式

5.1 数据展示 + 交互模式

jsx
// Server Component负责数据,Client Component负责交互
async function TodoList({ userId }) {
  const todos = await fetchTodos(userId);
  
  return (
    <div>
      <h2>待办事项</h2>
      
      {todos.map(todo => (
        <TodoItem 
          key={todo.id}
          todo={todo}
          onToggle={toggleTodo}  // Server Action
          onDelete={deleteTodo}  // Server Action
        />
      ))}
      
      <AddTodoForm addAction={addTodo} />
    </div>
  );
}

// Client Component
'use client';

function TodoItem({ todo, onToggle, onDelete }) {
  const [optimisticCompleted, setOptimisticCompleted] = useState(todo.completed);
  
  const handleToggle = async () => {
    // 乐观更新
    setOptimisticCompleted(!optimisticCompleted);
    
    try {
      await onToggle(todo.id);
    } catch (error) {
      // 回滚
      setOptimisticCompleted(optimisticCompleted);
    }
  };
  
  return (
    <div className={optimisticCompleted ? 'completed' : ''}>
      <input
        type="checkbox"
        checked={optimisticCompleted}
        onChange={handleToggle}
      />
      <span>{todo.title}</span>
      <button onClick={() => onDelete(todo.id)}>删除</button>
    </div>
  );
}

5.2 表单提交模式

jsx
// Server Actions
'use server';

export async function createPost(formData) {
  const title = formData.get('title');
  const content = formData.get('content');
  
  const post = await db.post.create({
    data: { title, content }
  });
  
  revalidatePath('/posts');
  redirect(`/posts/${post.id}`);
}

// Server Component
import { createPost } from './actions';

async function NewPostPage() {
  return (
    <div>
      <h1>新建文章</h1>
      <PostForm action={createPost} />
    </div>
  );
}

// Client Component
'use client';

function PostForm({ action }) {
  const [errors, setErrors] = useState({});
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    const formData = new FormData(e.target);
    
    // 客户端验证
    const title = formData.get('title');
    if (!title || title.length < 3) {
      setErrors({ title: '标题至少3个字符' });
      return;
    }
    
    // 调用Server Action
    try {
      await action(formData);
    } catch (error) {
      setErrors({ submit: '提交失败,请重试' });
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>标题</label>
        <input name="title" required />
        {errors.title && <span className="error">{errors.title}</span>}
      </div>
      
      <div>
        <label>内容</label>
        <textarea name="content" required />
      </div>
      
      {errors.submit && (
        <div className="error">{errors.submit}</div>
      )}
      
      <button type="submit">发布</button>
    </form>
  );
}

5.3 实时更新模式

jsx
// Server Component
async function ChatRoom({ roomId }) {
  const initialMessages = await fetchMessages(roomId);
  
  return (
    <div>
      <h2>聊天室</h2>
      
      {/* Client Component处理实时更新 */}
      <ChatMessages 
        roomId={roomId}
        initialMessages={initialMessages}
        sendAction={sendMessage}
      />
    </div>
  );
}

// Client Component
'use client';

function ChatMessages({ roomId, initialMessages, sendAction }) {
  const [messages, setMessages] = useState(initialMessages);
  
  useEffect(() => {
    // WebSocket连接
    const ws = new WebSocket(`/ws/chat/${roomId}`);
    
    ws.onmessage = (event) => {
      const newMessage = JSON.parse(event.data);
      setMessages(prev => [...prev, newMessage]);
    };
    
    return () => ws.close();
  }, [roomId]);
  
  const handleSend = async (text) => {
    // 乐观更新
    const tempMessage = {
      id: Date.now(),
      text,
      pending: true
    };
    setMessages(prev => [...prev, tempMessage]);
    
    try {
      await sendAction(roomId, text);
    } catch (error) {
      // 移除失败的消息
      setMessages(prev => prev.filter(m => m.id !== tempMessage.id));
    }
  };
  
  return (
    <div>
      <MessageList messages={messages} />
      <MessageInput onSend={handleSend} />
    </div>
  );
}

注意事项

1. Props必须可序列化

jsx
// ❌ 错误
async function Bad() {
  const handleClick = () => console.log('click');
  return <ClientComponent onClick={handleClick} />;
}

// ✅ 正确:使用Server Actions
import { handleAction } from './actions';

async function Good() {
  return <ClientComponent action={handleAction} />;
}

2. 避免过度Client化

jsx
// ❌ 不好:整个组件都是Client
'use client';

function ProductPage({ product }) {
  const [liked, setLiked] = useState(false);
  
  return (
    <div>
      {/* 大部分是静态内容 */}
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>¥{product.price}</p>
      
      {/* 只有这个按钮需要交互 */}
      <button onClick={() => setLiked(!liked)}>
        {liked ? '❤️' : '🤍'}
      </button>
    </div>
  );
}

// ✅ 更好:分离静态和动态部分
async function ProductPage({ id }) {
  const product = await fetchProduct(id);
  
  return (
    <div>
      {/* Server Component渲染静态内容 */}
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>¥{product.price}</p>
      
      {/* Client Component处理交互 */}
      <LikeButton productId={product.id} />
    </div>
  );
}

3. 合理使用Server Actions

jsx
// ✅ Server Actions适合
- 表单提交
- 数据变更
- 需要服务器验证的操作

// ❌ 不适合Server Actions
- 频繁的UI状态更新
- 纯客户端交互
- 不需要服务器的操作

常见问题

Q1: 如何在Client Component中获取新数据?

A: 使用Server Actions + revalidate,或者使用API路由。

Q2: 能在Client Component中导入Server Component吗?

A: 不能直接导入,但可以通过props(如children)传递。

Q3: Server Actions和API路由有什么区别?

A: Server Actions更简洁,自动处理序列化,但API路由更灵活,可以被非React客户端调用。

总结

交互规则

✅ Server → Client: 直接导入
❌ Client → Server: 不能直接导入
✅ 通过children传递: 可以
✅ 通过props传递: 数据必须可序列化
✅ 使用Server Actions: 推荐

最佳实践

1. 默认使用Server Components
2. 只在需要时使用Client Components
3. 使用Server Actions处理服务器操作
4. 保持组件边界清晰
5. 合理使用Context
6. 优化数据传递
7. 避免不必要的客户端代码

掌握Server和Client Components的交互是构建现代React应用的关键!