← [ BACK_TO_NOTES ]

Next.js 数据获取边界

把 Next.js Server Component、Client Component、独立服务 API、TanStack Query、SSE 与本地 UI 状态放在同一张数据获取地图中,按首屏需求、ownership 与运行期同步决定边界。

#synthesis #growing #frame #react

[!info] related notes

  • 所属 MOC:
  • 相关概念:
  • 易混淆概念:
  • 相关资源:

Next.js 数据获取边界

覆盖范围

这篇笔记不是重复介绍 fetch() 或 ORM,而是回答 Next.js App Router 中最关键的架构问题:

一份数据应该在哪一侧获取、通过什么边界获取、什么时候交给客户端继续同步?

需要同时看三条轴:

1. 首屏是否需要?
2. 谁拥有这份数据?
3. 页面运行后是否需要持续同步/交互?

第一条轴:首屏是否需要

如果数据决定页面初次展示内容,优先考虑 Server Component:

Request
  ↓
Server Component
  ↓
await data
  ↓
render useful initial UI

这通常比:

HTML / JS
→ hydrate
→ useEffect
→ fetch
→ setState
→ render

少一个浏览器启动后的首屏请求阶段。

具体机制见 Next.js Server Component 数据获取,更一般的 SSR 原理见 React SSR 数据获取位置。

第二条轴:谁拥有数据

Server Component 能访问数据库,不代表它应该绕过已有服务边界。

当前 Next.js 应用拥有领域
→ Server Component
→ data layer / repository
→ DB
独立 Go / Java / Python Service 拥有领域
→ Server Component
→ owning service API
→ domain/service layer
→ DB

判断依据见 数据所有权与服务边界。

第三条轴:页面运行后是否需要持续同步

如果数据在页面打开后还需要:

  • polling
  • refetch
  • infinite scroll
  • optimistic update
  • mutation 后 invalidation
  • focus/reconnect 后同步

则通常进入客户端 server-state 生命周期:

Client Component
      ↓
TanStack Query / SWR
      ↓
API

TanStack Query 的 cache 仍然只是 server-owned data 的客户端 projection,不是新的 durable truth。见 TanStack Query 服务端状态管理。

四类数据的默认落点

Application Data
│
├─ Server-rendered initial data
│    → Server Component
│
├─ Client-managed server state
│    → TanStack Query / SWR
│
├─ Realtime runtime data
│    → SSE / WebSocket client runtime
│
└─ Client-owned UI state
     → useState / useReducer / Zustand / localStorage

注意:这些是生命周期角色,不是互斥的数据实体分类。同一个业务对象可以先由 Server Component 提供初始 snapshot,再在客户端进入 Query/SSE 同步生命周期。

Server Fetch + Client Interaction

Client Component 不需要自己获取所有初始数据:

export default async function ProfilePage() {
  const profile = await getProfile()
  return <ProfileEditor initialProfile={profile} />
}
"use client"

function ProfileEditor({ initialProfile }) {
  const [name, setName] = useState(initialProfile.name)
  // interaction
}

因此:

Client Component
≠
Client Fetch

组件执行位置与数据获取位置需要分开判断。

Initial Snapshot + Live Runtime

AI/聊天应用特别适合把初始状态和实时状态分开:

Page load
→ Server Component
→ API
→ conversation history snapshot

页面进入交互后:

Client Component
→ SSE / WebSocket
→ incremental events

如果还存在 durable server-state cache,可以进一步组合:

Initial server snapshot
        ↓
Client projection
        ↕
TanStack Query reconciliation
        +
SSE transient/runtime updates

相关复杂流式状态见 TanStack Query 与 SSE 流式数据集成。

BodySense:Assessment Result

对于:

/assessment/123

错误心智:

Next.js 是服务器代码
→ 直接查 Assessment DB

正确判断:

Assessment domain owner = Go Backend
          ↓
Next.js Server Component
          ↓
Go Assessment API
          ↓
Service / permission / domain rules
          ↓
DB

因此它是Server Component 获取,但通过 owning service API,而不是直接 DB。

BodySense:Consultation History + SSE

初始历史:

Next.js Server Component
→ Go API
→ authoritative history

实时增量:

Client runtime
→ SSE
→ run/event updates

所以正确答案不是:

历史消息在数据库获取

而是:

历史消息最终来自服务端 durable storage,
但 Next.js 应通过拥有该领域的 Go API 获取。

这再次体现:storage location 与 access boundary 不是同一个问题。

Theme / localStorage

如果一份数据只存在浏览器:

localStorage
window
DOM
browser event

它天然属于 Client Runtime。

例如 ThemeSwitcher:

ThemeSwitcher
→ Client Component
→ localStorage

如果希望服务器首屏也知道 Theme,就应把初始偏好同步到 Cookie 等 request-visible storage,而不是期待 Server Component 直接读取浏览器 localStorage。

useEffect(fetch) 的位置

useEffect 不是“错误 API”,而是底层外部系统同步工具。

但对常见首屏 Server Data:

useEffect(fetch)

通常不应是默认选择,因为它会把请求推迟到浏览器 JS 启动之后。

对复杂 server-state lifecycle,也通常优先用 TanStack Query 等专门工具,而不是自己用 useEffect + useState 重写 cache/retry/stale/race handling。

因此:

首屏 server data
→ Server Component

复杂 client server-state
→ TanStack Query / SWR

简单浏览器副作用同步
→ useEffect

多个请求的执行策略

如果多个数据源互不依赖:

getProfile()
getAssessments()
getNotifications()

应避免 accidental waterfall,优先并行:

const [profile, assessments, notifications] = await Promise.all([
  getProfile(),
  getAssessments(),
  getNotifications(),
])

如果一个请求必须使用前一个结果,则顺序等待是合理 dependency waterfall。

完整模型见 Request Waterfall 与并行数据获取。

数据可以靠近消费者

Server Component 允许数据请求下沉到真正需要它的组件:

Dashboard
├─ ProfilePanel → getProfile()
├─ AssessmentPanel → getAssessments()
└─ AIInsightPanel → getAIInsight()

这有利于局部 Suspense / Streaming,但不代表每个小组件都应该产生一个独立 HTTP/SQL 请求。

见 数据获取共置。

数据获取与缓存必须分开理解

本篇只决定:

从哪里获取?
在哪里获取?
通过谁获取?
何时交给 Client?

缓存是另一个维度:

数据什么时候复用?
什么时候 stale?
什么时候 revalidate?
mutation 后如何失效?

Next.js Cache / Revalidation 应独立学习,不能把“Server Component 数据获取”直接等同于某一种缓存策略。

决策树

一份数据
│
├─ 只存在浏览器环境?
│    └─ 是 → Client Runtime
│
├─ 首屏就需要?
│    └─ 是 → 优先 Server Component
│
├─ 谁拥有它?
│    ├─ 当前 Next.js App → data layer / DB
│    └─ 独立 Domain Service → service API
│
├─ 页面运行后持续同步?
│    └─ 是 → TanStack Query / SWR / realtime
│
├─ 是实时事件?
│    └─ 是 → SSE / WebSocket client runtime
│
└─ 多个请求互不依赖?
     └─ 是 → 并行启动 / Promise.all

最短记忆

先问首屏,
再问 owner,
再问运行期是否持续同步。

Server Component 决定“在哪里开始拿数据”,
Service Boundary 决定“应该找谁拿”,
Client Server-State 决定“页面活起来后怎么继续同步”。
创建于 2026/9/15 更新于 2026/9/15
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本