React 内置了状态管理:useState + useContext。
为什么还需要 Zustand、Jotai、Valtio?
答案不在"它们更好用",而在 React 默认的更新传播模型有它的边界。
一、问题:React 的更新传播
1.1 setState 的传播范围
function App() {
const [count, setCount] = useState(0)
return (
<div>
<Header />
<Counter count={count} setCount={setCount} />
<HeavyList /> {/* 和 count 无关,但会重新渲染 */}
</div>
)
}
count 变化 → App 重新渲染 → Header、Counter、HeavyList 都重新渲染。
HeavyList 和 count 完全无关,但还是重新渲染了。
1.2 为什么 React 这样设计?
因为 React 的核心模型是:
UI = f(state)
state 变了 → 调用 f → 得到新的 UI 描述。
React 不知道"哪些组件依赖这个 state"。它只知道"state 变了,调用根组件函数,得到新的 JSX 树"。
这是 Description 模型的代价:React 不追踪依赖,只重新计算描述。
1.3 Vue 的对比
<template>
<div>
<Header />
<Counter :count="count" @increment="count++" />
<HeavyList />
<!-- count 变了不会重新渲染 -->
</div>
</template>
Vue 的响应式系统知道"只有 Counter 依赖 count"。count 变了 → 只有 Counter 重新渲染。
Vue 走的是 Dependency Tracking 模型:追踪谁依赖谁,精确更新。
二、React 的解法:bailout
React 不是完全没有优化。它有 bailout 机制:
function Counter({ count, onClick }) {
return <button onClick={onClick}>{count}</button>
}
function HeavyList() {
// 这个组件很重
return <div>{/* 渲染 1000 个 item */}</div>
}
function App() {
const [count, setCount] = useState(0)
return (
<div>
<Counter count={count} onClick={() => setCount((c) => c + 1)} />
<HeavyList />
</div>
)
}
App 重新渲染时:
- Counter 重新渲染(因为 count prop 变了)
- HeavyList 重新渲染(因为 App render 了)
- HeavyList 返回的 JSX 和上次一样 → bailout → DOM 不更新
bailout 避免了不必要的 DOM 操作,但组件函数还是执行了。
如果 HeavyList 的渲染函数本身就很重(比如大量计算),bailout 帮不了你。
三、memo:组件边界 bailout
const HeavyList = memo(function HeavyList() {
console.log('HeavyList render')
return <div>{/* 渲染 1000 个 item */}</div>
})
memo 让 HeavyList 连函数都不执行:
App render
↓
HeavyList: props 没变(=== 引用相等)
↓
bailout → 不执行函数 → 不进入 reconcile
但 memo 有一个前提:props 的引用要稳定。
// ❌ 每次 App render 都创建新对象,memo 失效
<HeavyList style={{ color: 'red' }} />
// ✅ useMemo 保持引用
const style = useMemo(() => ({ color: 'red' }), [])
<HeavyList style={style} />
React 的性能优化本质上只有一件事:让 memoizedProps === pendingProps 成立。
四、Context 的边界
Context 是 React 内置的"跨组件传递状态"方案:
const ThemeContext = createContext('light')
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
)
}
function Button() {
const theme = useContext(ThemeContext)
return <button className={theme}>Click</button>
}
4.1 Context 的更新传播
function App() {
const [theme, setTheme] = useState('light')
const [count, setCount] = useState(0)
return (
<ThemeContext.Provider value={theme}>
<CounterContext.Provider value={count}>
<Layout />
</CounterContext.Provider>
</ThemeContext.Provider>
)
}
theme 变化 → ThemeContext.Provider 的 value 变了 → 所有 useContext(ThemeContext) 的组件重新渲染。
count 变化 → CounterContext.Provider 的 value 变了 → 所有 useContext(CounterContext) 的组件重新渲染。
Context 的粒度是 Provider 级别的,不是 value 内部的属性级别。
4.2 Context 的问题
const UserContext = createContext({ name: '', email: '', avatar: '' })
function App() {
const [user, setUser] = useState({ name: 'Alice', email: 'alice@test.com', avatar: '...' })
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
)
}
function UserName() {
const { name } = useContext(UserContext)
return <div>{name}</div>
}
function UserAvatar() {
const { avatar } = useContext(UserContext)
return <img src={avatar} />
}
avatar 变了 → 整个 user 对象引用变了 → UserName 和 UserAvatar 都重新渲染。
UserName 只依赖 name,但 avatar 变了它也重新渲染了。
Context 无法做到"只订阅对象的某个属性"。
4.3 拆分 Context
const UserNameContext = createContext('')
const UserAvatarContext = createContext('')
function App() {
const [user, setUser] = useState(...)
return (
<UserNameContext.Provider value={user.name}>
<UserAvatarContext.Provider value={user.avatar}>
<Layout />
</UserAvatarContext.Provider>
</UserNameContext.Provider>
)
}
可以解决,但嵌套太多,维护成本高。
五、External Store:useSyncExternalStore
当 Context 不够用时,可以把状态放到 React 树外部。
5.1 外部 store 的基本模式
// store.js
let state = { count: 0, theme: 'light' }
const listeners = new Set()
function getState() {
return state
}
function setState(newState) {
state = { ...state, ...newState }
listeners.forEach((l) => l())
}
function subscribe(listener) {
listeners.add(listener)
return () => listeners.delete(listener)
}
5.2 useSyncExternalStore
React 18 提供了 useSyncExternalStore 来正确接入外部 store:
import { useSyncExternalStore } from 'react'
function Counter() {
const count = useSyncExternalStore(
subscribe, // 订阅函数
() => state.count // 获取快照(客户端)
() => state.count // 获取快照(SSR,可选)
)
return <div>{count}</div>
}
为什么需要 useSyncExternalStore 而不是直接用 useEffect + useState?
// ❌ 手动实现,有撕裂风险
function Counter() {
const [count, setCount] = useState(state.count)
useEffect(() => {
return subscribe(() => {
setCount(state.count)
})
}, [])
return <div>{count}</div>
}
在并发模式下,这个实现可能导致 Tearing(撕裂):同一个渲染周期内,不同组件读到的 store 值不一致。
useSyncExternalStore 保证了:
- 读取快照是同步的
- 所有组件看到一致的值
- SSR 时使用 server snapshot
5.3 selector 模式
function Counter() {
const count = useSyncExternalStore(
subscribe,
() => state.count, // selector:只取 count
)
return <div>{count}</div>
}
selector 让你只订阅 store 的一部分。count 变了才触发重新渲染,theme 变了不影响。
六、Zustand:最小化的 external store
Zustand 是对 useSyncExternalStore 的封装,提供了更简洁的 API:
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
theme: 'light',
increment: () => set((state) => ({ count: state.count + 1 })),
setTheme: (theme) => set({ theme }),
}))
function Counter() {
const count = useStore((state) => state.count)
const increment = useStore((state) => state.increment)
return <button onClick={increment}>{count}</button>
}
6.1 selector 自动优化
// 只有 count 变化时,这个组件才重新渲染
const count = useStore((state) => state.count)
// 只有 theme 变化时,这个组件才重新渲染
const theme = useStore((state) => state.theme)
Zustand 内部用 Object.is 比较 selector 返回值。值没变就不触发更新。
6.2 与 Context 的对比
| 维度 | Context | Zustand |
|---|---|---|
| 订阅粒度 | Provider 级别 | selector 级别 |
| 更新传播 | 所有 consumer | 只有匹配的 selector |
| 性能优化 | 需要拆分 Context | 自动 selector 优化 |
| 代码量 | 嵌套 Provider | create + useStore |
| SSR | 原生支持 | 需要额外配置 |
6.3 什么时候用 Zustand
- 全局状态:用户信息、主题、语言
- 跨组件共享:购物车、通知列表
- 频繁更新:鼠标位置、滚动位置
- Context 性能不够:Provider consumer 太多,更新传播太广
七、Jotai:原子化状态
Jotai 走了另一条路:把状态拆成 atom,依赖自动追踪。
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
const doubledAtom = atom((get) => get(countAtom) * 2)
function Counter() {
const [count, setCount] = useAtom(countAtom)
const doubled = useAtom(doubledAtom)[0]
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<span>× 2 = {doubled}</span>
</div>
)
}
7.1 atom 的依赖追踪
const countAtom = atom(0)
const doubledAtom = atom((get) => get(countAtom) * 2)
const tripledAtom = atom((get) => get(countAtom) * 3)
function Doubled() {
const doubled = useAtom(doubledAtom)[0]
return <div>{doubled}</div> // count 变了才重新渲染
}
function Tripled() {
const tripled = useAtom(tripledAtom)[0]
return <div>{tripled}</div> // count 变了才重新渲染
}
Jotai 知道"哪些 atom 依赖哪些 atom"。countAtom 变了 → 只有依赖它的 doubledAtom 和 tripledAtom 触发更新。
7.2 Jotai vs Zustand
| 维度 | Zustand | Jotai |
|---|---|---|
| 模型 | 单一 store + selector | 多个 atom + 自动依赖追踪 |
| 适合场景 | 中等复杂度的状态 | 复杂的、相互依赖的状态 |
| 性能优化 | 手动 selector | 自动依赖追踪 |
| 学习曲线 | 低 | 中(atom 概念) |
7.3 什么时候用 Jotai
- 状态相互依赖:A 依赖 B,B 依赖 C
- 动态状态:运行时创建的 atom
- 细粒度更新:需要精确控制哪个组件更新
八、选型决策树
你的状态需要跨组件共享吗?
│
├── 否 → useState / useReducer(组件内状态)
│
└── 是 → 状态是服务端数据吗?
│
├── 是 → TanStack Query / SWR(数据获取层)
│
└── 否 → 更新频率高吗?
│
├── 否 → Context(简单共享)
│
└── 是 → 状态结构复杂吗?
│
├── 否 → Zustand(store + selector)
│
└── 是 → Jotai(atom + 自动依赖追踪)
九、实战:组合使用
实际项目中,不同类型的状态用不同的方案:
// 服务端数据 → TanStack Query
function UserProfile({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
})
return <div>{user.name}</div>
}
// 全局 UI 状态 → Zustand
const useUIStore = create((set) => ({
sidebarOpen: true,
theme: 'light',
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen }))
}))
function Layout() {
const sidebarOpen = useUIStore((s) => s.sidebarOpen)
return <div className={sidebarOpen ? 'sidebar-open' : ''}>...</div>
}
// 复杂派生状态 → Jotai
const filtersAtom = atom({ category: 'all', sort: 'newest' })
const filteredItemsAtom = atom((get) => {
const items = get(itemsAtom)
const filters = get(filtersAtom)
return applyFilters(items, filters)
})
function ItemList() {
const filtered = useAtom(filteredItemsAtom)[0]
return <div>{filtered.map(...)}</div>
}
// 组件内局部状态 → useState
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
十、与五个 Law 的关系
| Law | 状态管理的角色 |
|---|---|
| Law 1 (Environment) | 外部 store 在 Server 和 Client 都能用(useSyncExternalStore) |
| Law 2 (Description) | 状态变化触发 Description 重新计算 |
| Law 3 (Ownership) | 谁拥有状态?组件内 vs 外部 store vs Context |
| Law 4 (Lane) | 状态更新的优先级由触发方式决定 |
| Law 5 (Cost) | 状态更新的传播范围影响渲染成本 |
十一、总结
状态管理的本质问题是:
谁拥有状态?状态变化时,哪些组件需要知道?
| 方案 | 状态归属 | 更新粒度 |
|---|---|---|
| useState | 组件内 | 组件级 |
| Context | Provider 级 | 所有 consumer |
| Zustand | 外部 store | selector 级 |
| Jotai | atom 级 | 依赖自动追踪 |
| TanStack Query | 服务端缓存 | query 级 |
没有最好的方案,只有最适合场景的方案。
关键是理解每种方案的边界,然后在正确的场景使用正确的工具。
这就是状态管理的 Ownership。