·769 字 / 约 2 分钟
单页应用里的「状态放哪里」其实只有三个答案
URL、组件树、全局 store —— 犹豫不决的时候,按这个顺序从上往下试,基本不会错。
前端React架构
结论先放前面
在 React 应用里决定一个状态该放哪里时,按这个优先级从上往下问自己:
| 优先级 | 位置 | 判断依据 |
|---|---|---|
| 1 | URL | 刷新/分享/后退之后,用户是否期望它还在? |
| 2 | 组件树(就近) | 只有一个子树关心它吗? |
| 3 | 全局 store | 以上都不满足,且跨多个不相邻的子树 |
1. URL 是第一候选
大部分"该不该上全局状态"的纠结,答案其实是 URL。
筛选条件、当前分页、选中的 Tab、打开的是哪条详情——这些状态天然属于 URL:
- 用户刷新后希望它还在 ✅
- 用户希望把链接发给别人 ✅
- 浏览器后退键应该能撤销这次操作 ✅
放到 URL 里,这三个需求全部免费拿到。放到 store 里,你得自己实现一遍持久化和历史栈。
// 筛选条件放进 query string,而不是 useState + useEffect 同步
const [params, setParams] = useSearchParams()
const status = params.get('status') ?? 'all'
2. 就近放,别急着往上提
一个状态如果只被一个组件和它的子组件使用,那就让它待在最近的那个共同祖先里。
常见反模式是"以后可能会用到,先放 store 吧"。这个"以后"通常不会来,但它带来的是永久的额外复杂度:
- 需要为它写 action、reducer、类型
- 组件不再自洽,无法单独理解
- 测试时需要先构造 store
等到真的需要共享时再提升,是成本更低的路径。重构一个 useState 到 store 是机械操作;反过来把 store 拆掉则要动一片。
3. 全局 store 要克制
真正需要全局状态的东西其实不多:
- 用户身份与权限
- 主题、语言这类贯穿整个应用外壳的偏好
- 跨路由共享的草稿数据
选型上,我倾向 Zustand 而不是 Redux Toolkit——不是因为 Redux 不好,而是因为大部分项目根本不需要那么多约束。Zustand 的 store 就是一个 hook,心智负担小得多。
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export const useProfile = create(
persist(
(set) => ({
name: '',
setName: (name: string) => set({ name }),
}),
{ name: 'profile' },
),
)
一个自检清单
拿不准的时候,问这几个问题:
- 刷新页面后它应该保留吗?→ URL
- 它能被"撤销"吗(后退键)?→ URL
- 只有一棵子树关心它吗?→ 就近
useState - 它会在应用外壳层面被读到吗?→ store
四个都答"否",那这个状态可能压根不需要存在——它大概率是可以推导出来的派生值。