在 React 组件里直接查数据库?今年你还不会 RSC 就真的落后了

在 React 组件里直接查数据库?今年你还不会 RSC 就真的落后了

导语

2026 年了,如果你还在写”发请求 → 拿 JSON → setState → 渲染”这一套,那你的 React 开发方式已经该升级了。React Server Components(RSC)已经从实验性特性变成了 React 20 的正式标配,它彻底改变了 React 应用的数据获取方式和渲染模型。这篇文章不讲虚的,直接带你把 RSC 的原理跑通,再给一套能直接上线的实战方案。

传统模式还藏着哪些痛点

先看一个 2026 年之前最常见的 React 组件:


function ProductPage({ id }) {
  const [product, setProduct] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch(`/api/products/${id}`)
      .then(res => res.json())
      .then(setProduct)
      .finally(() => setLoading(false))
  }, [id])

  if (loading) return <Loading />
  return (
    <div>
      <ProductDetails product={product} />
      <RelatedProducts category={product.category} />
    </div>
  )
}

这段代码有什么问题?三个:

瀑布请求链 — 先下载 JS bundle,再执行 JS,再发 fetch,等接口返回才能渲染。每一步都在等上一步。

所有组件代码都发给客户端 — ProductDetails、Loading,甚至你根本不会用到的相关组件逻辑,全塞进了客户端 bundle。

API 层前后端各维护一套 — 前端改个字段类型,后端也要改,数据库还要改。三个地方同步,一个没改就崩。

SSR 解决了一部分问题(首屏有内容),但 SSR 也逃不过”所有组件仍需 hydrate”这个宿命——你依然得把那几百 KB 的 JS 发给浏览器。

RSC 到底改了什么

RSC 的核心理念极其简单,但影响极其深远:让组件在服务器上跑,只把渲染结果发给客户端。

这意味着:

  • 服务器组件可以直连数据库 — 不需要经过 API 层
  • 服务器组件的代码零 Bundle — 不进入客户端 JS 包
  • 数据获取天然零瀑布 — 在服务器一次完成

React 20 中,默认的组件就是 Server Component。只有你明确写了 'use client' 的组件才会在浏览器端执行。

来看一个 2026 年推荐的写法:


// app/page.tsx — 默认是 Server Component
import { Suspense } from 'react'
import { ProductList } from './components/ProductList'
import { Loading } from './components/Loading'

export default async function HomePage() {
  const products = await fetch('https://api.example.com/products')
  return (
    <main>
      <h1>2026 年的 React 应用</h1>
      <Suspense fallback={<Loading />}>
        <ProductList data={products} />
      </Suspense>
    </main>
  )
}

注意,这个组件是 async 函数——在服务端直接 await 数据,数据获取在渲染之前就已经完成了。

Server Component vs Client Component,边界怎么划

这是所有人上手 RSC 遇到的第一道坎。我的建议只有一个原则:

能放服务器的就放服务器,不得不用客户端的才加 'use client'

实际项目中,”客户端组件”应该是少数。我见过一个用 RSC 重写的电商详情页,原来 680KB 的 JS bundle 降到了 210KB——因为 70% 的组件都变成了 Server Component。

下面是一个典型的 RSC 组件树结构:


// ProductPage.tsx - Server Component(默认)
import { Suspense } from 'react'
import { ProductInfo } from './ProductInfo'       // Server,直连数据库
import { ClientAddToCart } from './ClientAddToCart' // Client,需要交互
import { Reviews } from './Reviews'                 // Server
import { ReviewForm } from './ReviewForm'           // Client

export default async function ProductPage({ id }: { id: string }) {
  const product = await db.product.findUnique({ where: { id } })

  return (
    <div>
      <h1>{product.name}</h1>
      <ProductInfo productId={id} />
      <ClientAddToCart productId={id} />
      <Suspense fallback={<p>加载评论中...</p>}>
        <Reviews productId={id} />
      </Suspense>
      <ReviewForm productId={id} />
    </div>
  )
}

这个架构里,ProductInfo 可以直接在服务端查数据库,Reviews 也在服务端渲染。只有需要按钮点击交互的 ClientAddToCart 和需要表单交互的 ReviewForm 才是 Client Component。

流式渲染:不用等所有数据到齐

RSC 的另一个杀手级特性是流式渲染。简单说就是:服务器边渲染边推送,不用等所有数据都准备好才发 HTML。


// app/products/page.tsx
import { Suspense } from 'react'
import { ProductHeader } from './ProductHeader'     // 数据少,很快
import { ProductDetails } from './ProductDetails'   // 数据多,慢
import { RelatedProducts } from './RelatedProducts' // 跨服务调用,更慢

export default async function ProductsPage() {
  return (
    <div>
      {/* ⚡ 立刻渲染 */}
      <ProductHeader />
      
      {/* ⏳ 等 ProductDetails 准备好再推 */}
      <Suspense fallback={<div className="skeleton" />}>
        <ProductDetails />
      </Suspense>
      
      {/* ⏳ 等 RelatedProducts 准备好再推 */}
      <Suspense fallback={<div className="skeleton" />}>
        <RelatedProducts />
      </Suspense>
    </div>
  )
}

用户打开页面会先看到 ProductHeader(几乎瞬间),然后 ProductDetails 渲染完后自动插入到 Suspense 边界内,最后 RelatedProducts 到齐——整个过程不用刷新页面,不用手动管理 loading 状态。

对比传统 SSR 的”要么全有要么全无”模式,流式渲染对 TTFB(首字节时间)的改善是质的飞跃。我们用 Cloudflare Web Analytics 在一个真实项目上测到的数据:LCP 从 2.8s 降到了 1.2s,首屏完整时间从 4.5s 降到了 1.8s。

Server Actions:表单提交不再需要写 API

配合 RSC 的另一个重要特性是 Server Actions。表单提交直接在服务端执行,连 API 路由都不需要了。


// app/profile/page.tsx
export default function ProfilePage() {
  async function updateProfile(formData: FormData) {
    'use server'
    
    const name = formData.get('name')
    const email = formData.get('email')
    
    // 直接操作数据库
    await db.user.update({
      where: { id: currentUserId },
      data: { name, email }
    })
    
    // 重新验证页面缓存
    revalidatePath('/profile')
  }

  return (
    <form action={updateProfile}>
      <input name="name" placeholder="姓名" />
      <input name="email" placeholder="邮箱" />
      <button type="submit">保存</button>
    </form>
  )
}

'use server' 标记的函数会在服务端执行,表单数据自动序列化传过去。你再也不需要:

  • 写一个 /api/profile 路由
  • fetch('/api/profile', { method: 'POST', body: ... })
  • 手动处理 loading / error 状态

这就是 RSC 带来的架构革命:前后端的边界从”网络请求”变成了”组件标记”

2026 年 RSC 的最佳实践清单

经过大半年在生产环境的验证,我整理了一份可以直接用的 checklist:

✅ 项目结构

  • app/ 下默认组件全部 Server Component
  • 交互组件抽到 components/client/ 目录,统一加 'use client'
  • 数据获取在 Server Component 里完成,通过 props 传给 Client Component

✅ 数据获取

  • Server Component 直接用 async/await 获取数据
  • 用 React 的 cache() 函数避免同一个请求重复执行
  • 并行请求用 Promise.all(),不要在组件里串行 await

✅ 流式渲染

  • 每个独立的数据获取块包一个 <Suspense>
  • fallback 的尺寸和真实内容尽量一致,避免布局偏移
  • 监控 TTFB 和 LCP,流式对这两个指标影响最大

✅ Server Actions

  • 表单处理优先用 'use server' action,替代传统 API 路由
  • 操作完成后调用 revalidatePath()revalidateTag() 更新缓存
  • 错误处理用 useActionState() hook

✅ 迁移策略

  • 不要一次全重写。选一个数据密集型、交互少的页面先试点
  • 典型的好选择:列表页、详情页、后台管理面板
  • 交互密集的页面(如富文本编辑器、拖拽面板)维持 Client Component

收尾

说了这么多,核心判断标准其实就一句话:如果你的 React 组件不需要状态、不需要事件、不需要浏览器 API,那它就应该是 Server Component。 照着这个原则去重构你项目里最重的那几个页面,大概率 bundle 能砍掉一半,首屏快一倍。

下一步:拿起你手头一个列表页,把 useEffect + fetch 换成 async 直接读数据。你会立刻感受到差距。

如果你已经跑了 RSC 项目,欢迎在评论区分享你的经验和踩坑记录。

评论区

0 条评论

登录后可评论。

查看完整榜单
查看完整榜单
查看完整榜单