Next.js 数据获取边界
把 Next.js Server Component、Client Component、独立服务 API、TanStack Query、SSE 与本地 UI 状态放在同一张数据获取地图中,按首屏需求、ownership 与运行期同步决定边界。
[!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 决定“页面活起来后怎么继续同步”。