← [ BACK_TO_NOTES ]

组件内服务端状态与客户端状态的区分

区分 React 组件中的 server-owned projection 与 client-owned UI state;判断 ownership 以 authority/lifetime 为准,而不是看值存放在 useState、Zustand 还是 Query Cache。

#concept #growing #frontend #react

[!info] related notes

组件内服务端状态与客户端状态的区分

一句话定义

React 里的“state”不都由前端拥有。来自服务器的数据即使被放进 useState 或 Zustand,也通常只是 server-owned fact 的客户端 projection;Dialog、Accordion、未提交表单等才是典型 client-owned UI state。

最重要的判断:冲突时谁裁决

不要只问:

这个值现在存在哪?

要问:

如果客户端和服务器两份值冲突,谁有资格决定哪一份是真的?

例如:

Query cache: revision = 42
Server:      revision = 43

客户端必须把 42 当成 stale projection,而不是把服务器改回 42。

所以:

Storage/container
≠ ownership/authority

常见五类状态

A. Local component state
   Dialog open、Accordion、未提交输入

B. URL state
   route resource id、分页、可分享筛选

C. Shared client presentation state
   sidebar collapsed、主题、跨组件 UI workflow

D. Server state/cache projection
   /api/me、HealthWorkspace、conversation list

E. Durable domain truth
   DB 中的用户/Workspace、服务器 current_revision

D 和 E 内容可能一模一样,但 ownership 不一样:D 是缓存/投影,E 是权威事实。

为什么服务端数据不适合复制成独立 useState

例如:

const [conversation, setConversation] = useState<Conversation | null>(null);

useEffect(() => {
  fetchConversation(id).then(setConversation);
}, [id]);

很快就需要手工处理:

loading/error
retry
request dedup
stale data
old response race
refetch
mutation synchronization
cache invalidation

这等于在组件里重写一个简陋的 server-state synchronization layer。

因此常见做法是:

server projection
→ TanStack Query / Router loader / dedicated data layer

local UI state
→ useState/useReducer

但不要把规则背成“后端有就一定 TanStack Query”

TanStack Query 是常见而合适的 server-state cache/sync 工具,但这不是 ownership 定义本身。

例如 /api/me 的 user projection 可能因为认证流程被暂存在 Zustand;这仍然是 server-owned data stored in a client container。

真正危险的是:

server copy
+
query copy
+
zustand copy

都能被客户端独立修改,却没有明确 reconciliation 规则。

Identity 与 Data 也要分开

一个常见竞态是同时维护:

URL: /conversation/abc
+
currentConversation.id = xyz

这形成两套“当前资源是谁”的 identity source。

更稳的是:

URL/Router
→ current resource identity

Query
→ data for that identity

见 URL 作为唯一身份源。

Query lifecycle state 不是普通 UI state

isPending、isFetching、error 虽然最终会影响 UI,但它们描述的是 Query 生命周期,通常应由数据层提供,而不是组件再复制一份:

isPending
≈ 尚无成功可用的 Query 数据状态

isFetching
≈ 当前有 fetch 正在执行

已有缓存时可以:

data exists
isFetching = true

此时不应该因为后台 refetch 就清空页面内容。

Local / Shared Client State 什么时候升级

只在一个组件交互
→ useState/useReducer

多个远距离组件需要共享,且由客户端拥有
→ Context/Zustand/Redux 等

可导航、可分享、刷新应恢复
→ URL/Router

不要因为“以后可能会用”就把局部状态提前全局化。

最短记忆

先问 owner,再选 container。

Local UI    → component
URL identity→ router
Shared UI   → client store
Server data → server-state cache/projection
Durable fact→ server

如果副本冲突,谁裁决,谁才是 authority。
创建于 2026/7/2 更新于 2026/9/16
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
关闭浮层 / 取消命令;输入框内清空文本