Q5: 什么是 React Server Components (RSC)?它和 Client Components 的根本区别是什么?如何在 Next.js

55 阅读2分钟

Next.js 面试题详细答案 - Q5

Q5: 什么是 React Server Components (RSC)?它和 Client Components 的根本区别是什么?如何在 Next.js 中定义它们?('use client' 指令)

React Server Components (RSC) 概念

React Server Components 是一种新的 React 组件类型,它们在服务器上渲染,而不是在客户端。这是 React 18+ 和 Next.js 13+ 的重要特性。

RSC vs Client Components 根本区别

1. 渲染位置
// Server Component (默认)
async function BlogPost({ slug }) {
  const post = await fetch(`/api/posts/${slug}`)
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

// Client Component (需要 'use client')
;('use client')
function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false)
  return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>
}
2. 功能限制对比
特性Server ComponentsClient Components
渲染位置服务器浏览器
状态管理❌ 无 useState✅ 支持 useState
生命周期❌ 无 useEffect✅ 支持 useEffect
事件处理❌ 无 onClick✅ 支持 onClick
浏览器 API❌ 无 window✅ 支持 window
数据获取✅ 直接 fetch❌ 需要 useEffect
文件系统✅ 直接访问❌ 无法访问
数据库✅ 直接连接❌ 需要 API

详细功能对比

Server Components 特性
// ✅ 可以直接获取数据
async function ProductList() {
  const products = await db.products.findMany()
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  )
}

// ✅ 可以直接访问文件系统
import fs from 'fs'
import path from 'path'

async function BlogPost({ slug }) {
  const filePath = path.join(process.cwd(), 'posts', `${slug}.md`)
  const content = fs.readFileSync(filePath, 'utf8')
  return <div>{content}</div>
}

// ✅ 可以直接连接数据库
import { db } from './lib/db'

async function UserProfile({ userId }) {
  const user = await db.user.findUnique({
    where: { id: userId },
  })
  return <div>{user.name}</div>
}

// ❌ 不能使用状态
function ServerComponent() {
  // const [count, setCount] = useState(0) // 错误!
  return <div>静态内容</div>
}

// ❌ 不能使用事件处理
function ServerComponent() {
  return <button onClick={() => alert('click')}> // 错误! 点击我</button>
}
Client Components 特性
'use client'
import { useState, useEffect } from 'react'

// ✅ 可以使用状态
function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>计数: {count}</button>
}

// ✅ 可以使用生命周期
function DataFetcher() {
  const [data, setData] = useState(null)

  useEffect(() => {
    fetch('/api/data')
      .then((res) => res.json())
      .then(setData)
  }, [])

  return <div>{data ? data.message : '加载中...'}</div>
}

// ✅ 可以使用浏览器 API
function WindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 })

  useEffect(() => {
    const updateSize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      })
    }

    window.addEventListener('resize', updateSize)
    updateSize()

    return () => window.removeEventListener('resize', updateSize)
  }, [])

  return (
    <div>
      窗口大小: {size.width} x {size.height}
    </div>
  )
}

// ❌ 不能直接访问数据库
function ClientComponent() {
  // const user = await db.user.findMany() // 错误!
  return <div>需要通过 API 获取数据</div>
}

在 Next.js 中定义组件

1. Server Components (默认)
// app/page.js - 默认是 Server Component
export default function HomePage() {
  return <h1>首页</h1>
}

// app/blog/page.js - 异步 Server Component
export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
  return (
    <div>
      <h1>博客</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}
2. Client Components (需要 'use client')
// components/InteractiveButton.js
'use client'
import { useState } from 'react'

export default function InteractiveButton() {
  const [clicked, setClicked] = useState(false)

  return (
    <button onClick={() => setClicked(!clicked)}>
      {clicked ? '已点击' : '点击我'}
    </button>
  )
}

// components/Form.js
'use client'
import { useState } from 'react'

export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  })

  const handleSubmit = (e) => {
    e.preventDefault()
    // 处理表单提交
    console.log(formData)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="姓名"
        value={formData.name}
        onChange={(e) => setFormData({
          ...formData,
          name: e.target.value
        })}
      />
      <button type="submit">提交</button>
    </form>
  )
}

混合使用示例

// app/blog/[slug]/page.js - Server Component
import InteractiveButton from '@/components/InteractiveButton'
import LikeButton from '@/components/LikeButton'

export default async function BlogPost({ params }) {
  // 在服务器获取数据
  const post = await fetch(`/api/posts/${params.slug}`)

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>

      {/* 嵌入 Client Components */}
      <div className="actions">
        <LikeButton postId={post.id} />
        <InteractiveButton />
      </div>
    </article>
  )
}

// components/LikeButton.js - Client Component
'use client'
import { useState } from 'react'

export default function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false)
  const [count, setCount] = useState(0)

  const handleLike = async () => {
    const newLiked = !liked
    setLiked(newLiked)
    setCount(count + (newLiked ? 1 : -1))

    // 调用 API 更新点赞状态
    await fetch(`/api/posts/${postId}/like`, {
      method: 'POST',
      body: JSON.stringify({ liked: newLiked })
    })
  }

  return (
    <button onClick={handleLike}>
      {liked ? '❤️' : '🤍'} {count}
    </button>
  )
}

组件边界和传递

1. 组件边界
// Server Component 不能直接包含 Client Component 的状态逻辑
// ❌ 错误示例
async function ServerComponent() {
  const [state, setState] = useState(0) // 错误!
  return <div>{state}</div>
}

// ✅ 正确示例
async function ServerComponent() {
  return <ClientComponent />
}

;('use client')
function ClientComponent() {
  const [state, setState] = useState(0)
  return <div>{state}</div>
}
2. 数据传递
// Server Component 向 Client Component 传递数据
async function BlogPost({ params }) {
  const post = await fetch(`/api/posts/${params.slug}`)

  return (
    <div>
      <h1>{post.title}</h1>
      {/* 传递服务器数据给客户端组件 */}
      <CommentSection postId={post.id} comments={post.comments} />
    </div>
  )
}

;('use client')
function CommentSection({ postId, comments }) {
  const [newComment, setNewComment] = useState('')

  return (
    <div>
      <h3>评论</h3>
      {comments.map((comment) => (
        <div key={comment.id}>{comment.text}</div>
      ))}
      <textarea
        value={newComment}
        onChange={(e) => setNewComment(e.target.value)}
      />
    </div>
  )
}

最佳实践

1. 组件选择原则
// 优先使用 Server Components
// ✅ 静态内容、数据获取
async function ProductList() {
  const products = await db.products.findMany()
  return <ProductGrid products={products} />
}

// 必要时使用 Client Components
// ✅ 交互、状态管理
;('use client')
function ProductGrid({ products }) {
  const [selectedProduct, setSelectedProduct] = useState(null)

  return (
    <div>
      {products.map((product) => (
        <ProductCard
          key={product.id}
          product={product}
          onClick={() => setSelectedProduct(product)}
        />
      ))}
    </div>
  )
}
2. 性能优化
// 将 Client Components 放在组件树的叶子节点
async function BlogPost({ params }) {
  const post = await fetch(`/api/posts/${params.slug}`)

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      {/* 只在需要交互的地方使用 Client Component */}
      <LikeButton postId={post.id} />
    </article>
  )
}

总结

React Server Components 和 Client Components 的根本区别:

  1. 渲染位置:服务器 vs 浏览器
  2. 功能限制:Server Components 无状态和事件,Client Components 无直接数据访问
  3. 性能影响:Server Components 减少客户端包大小,Client Components 提供交互性
  4. 使用场景:Server Components 适合数据展示,Client Components 适合用户交互

通过合理使用两种组件类型,可以构建既高性能又交互丰富的应用。