React状态管理方案对比深度解析
全面对比 Redux、Zustand、Jotai 三大 React 状态管理方案,从设计理念到源码分析,帮助你构建清晰的状态管理选型思维。
一句话概括
React 状态管理正在从 Redux 的集中式 store 向 Zustand 的简洁 API 和 Jotai 的原子化方向演进,理解这三种方案的设计哲学和适用场景,是构建可维护 React 应用的关键能力。
背景与意义
React 的状态管理之争是一个持续了近十年的话题。从最初的 setState 到 Redux 的统一管理,再到 Context API 的回归,再到 Zustand 和 Jotai 等新一代方案的崛起,React 状态管理经历了「从混乱到有序再到多元化」的过程。
React 本身是一个 UI 库,它没有像 Vue 那样内置响应式系统。React 的状态更新机制是通过「不可变更新 + 重新渲染」来驱动的——每次状态变化都会触发整个组件的重新渲染(除非使用 React.memo)。这意味着选择合适的状态管理方案不仅是方便的 API 问题,更直接影响应用的渲染性能。
概念与定义
单向数据流:数据在应用中单向流动——View 触发 Action,Action 更新 State,State 驱动 View 更新。React 本身是单向数据流的,状态管理方案是对这种模式的延伸。
集中式 Store:所有的应用状态存储在一个单一的 Store 中(如 Redux)。优点是状态变化可追踪、可调试;缺点是 store 可能变得臃肿。
原子化状态:将状态拆分为最小单位(Atom),组件按需订阅自己关心的原子。当某原子变化时,只重新渲染订阅了该原子的组件。
不可变更新:每次状态更新都创建一个新的状态对象,而不是修改原对象。Redux 和 Zustand 都要求不可变更新。
最小示例
Redux Toolkit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// store.js - Redux Toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 },
},
})
export const { increment, decrement } = counterSlice.actions
export const store = configureStore({ reducer: counterSlice.reducer })
Zustand:
1
2
3
4
5
6
7
8
// store.js - Zustand
import { create } from 'zustand'
export const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
Jotai:
1
2
3
4
5
// atoms.js - Jotai
import { atom, useAtom } from 'jotai'
export const countAtom = atom(0)
// 使用:const [count, setCount] = useAtom(countAtom)
核心知识点拆解
1. Redux Toolkit:工业级标准方案
Redux Toolkit(RTK)是 Redux 的官方推荐写法,它解决了 Redux 原始写法的样板代码问题。
createSlice 与 Immer 集成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Redux Toolkit 核心:createSlice
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
// 异步 Action
export const fetchUsers = createAsyncThunk(
'users/fetchAll',
async (_, { rejectWithValue }) => {
try {
const response = await fetch('/api/users')
return await response.json()
} catch (err) {
return rejectWithValue(err.message)
}
}
)
const usersSlice = createSlice({
name: 'users',
initialState: {
items: [],
loading: 'idle', // 'idle' | 'pending' | 'succeeded' | 'failed'
error: null,
},
reducers: {
// 普通的同步 reducer
addUser: (state, action) => {
// 这里看起来是「修改」,但 RTK 通过 Immer 转成了不可变更新
state.items.push(action.payload)
},
clearUsers: (state) => {
state.items = []
state.loading = 'idle'
},
},
extraReducers: (builder) => {
// 处理 createAsyncThunk 的生命周期
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = 'pending'
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = 'succeeded'
state.items = action.payload
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = 'failed'
state.error = action.payload
})
},
})
export const { addUser, clearUsers } = usersSlice.actions
export default usersSlice.reducer
RTK Query——数据获取的终极方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// api.js - RTK Query
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User', 'Post'],
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
providesTags: ['User'],
}),
getUserById: builder.query({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
updateUser: builder.mutation({
query: ({ id, ...data }) => ({
url: `/users/${id}`,
method: 'PUT',
body: data,
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'User', id },
'User', // 同时刷新列表
],
}),
}),
})
export const { useGetUsersQuery, useGetUserByIdQuery, useUpdateUserMutation } = api
2. Zustand:极简主义的拥抱者
Zustand(德语中「状态」的意思)以约 1KB 的体积提供了一个极致简洁的 API:
快速上手:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
// 带中间件的 store
const useStore = create(
devtools(
persist(
(set, get) => ({
// state
user: null,
token: null,
notifications: [],
// actions
login: async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
})
const data = await response.json()
// set 可以传入部分状态,自动合并
set({
user: data.user,
token: data.token,
})
},
logout: () => {
set({ user: null, token: null })
// get() 可以获取当前状态
console.log('User logged out, was:', get().user?.name)
},
addNotification: (notification) => {
set((state) => ({
notifications: [...state.notifications, notification],
}))
},
markAllRead: () => {
set({ notifications: [] })
},
}),
{
name: 'auth-storage', // localStorage key
partialize: (state) => ({
// 只持久化需要持久化的字段
user: state.user,
token: state.token,
}),
}
),
{ name: 'MyAppStore' }
)
)
export default useStore
避免不必要的渲染:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ❌ 反例:组件订阅了所有状态
function Profile() {
const user = useStore((state) => state.user)
const notifications = useStore((state) => state.notifications)
// 当 notifications 变化时,这个组件也会重新渲染
// 即使它只关心 user
}
// ✅ 正解:精确选择
const user = useStore((state) => state.user)
// 或者使用 shallow 比较
import { shallow } from 'zustand/shallow'
function Profile() {
const { user, login } = useStore(
(state) => ({ user: state.user, login: state.login }),
shallow // 浅比较:user 没变就不重新渲染
)
}
状态切片模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 将大 store 拆分为多个小 store
import { create } from 'zustand'
// 创建独立的 store 切片
const useAuthStore = create((set) => ({
user: null,
token: null,
login: (user, token) => set({ user, token }),
logout: () => set({ user: null, token: null }),
}))
const useCartStore = create((set, get) => ({
items: [],
addProduct: (product) =>
set((state) => ({ items: [...state.items, product] })),
checkout: () => {
const items = get().items
const user = useAuthStore.getState().user // 跨 store 访问
// ...
},
}))
const useNotificationStore = create((set) => ({
messages: [],
push: (msg) => set((s) => ({ messages: [...s.messages, msg] })),
dismiss: (id) =>
set((s) => ({ messages: s.messages.filter((m) => m.id !== id) })),
}))
3. Jotai:原子化的先锋
Jotai 受 Recoil 启发,但更简洁、更轻量。它的核心思想是「原子即状态」——每个状态都是一个独立的原子,组件按需订阅。
原子基础:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
// 基础原子
const countAtom = atom(0)
const textAtom = atom('hello')
const userAtom = atom({ name: 'Alice', age: 30 })
// 派生原子(读)
const doubleCountAtom = atom((get) => get(countAtom) * 2)
// 派生原子(读写)
const userDisplayAtom = atom(
(get) => `${get(userAtom).name} (${get(userAtom).age})`,
(get, set, newName) => {
set(userAtom, { ...get(userAtom), name: newName })
}
)
// 异步原子
const userDataAtom = atom(async (get) => {
const userId = get(userIdAtom)
const response = await fetch(`/api/users/${userId}`)
return response.json()
})
// 组件中使用
function Counter() {
// 读
const [count, setCount] = useAtom(countAtom)
// 只读
const doubleCount = useAtomValue(doubleCountAtom)
// 只写
const setUserDisplay = useSetAtom(userDisplayAtom)
return (
<div>
<p>{count} (double: {doubleCount})</p>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</div>
)
}
原子化的优势——精确重渲染:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { atom, useAtom } from 'jotai'
// 两个独立的原子
const firstNameAtom = atom('')
const lastNameAtom = atom('')
// 派生原子
const fullNameAtom = atom((get) =>
`${get(firstNameAtom)} ${get(lastNameAtom)}`
)
// 组件 A:只关心 firstName
function FirstNameInput() {
const [firstName, setFirstName] = useAtom(firstNameAtom)
// 只有 firstNameAtom 变化时才会重新渲染
// lastNameAtom 的变化不会影响这个组件
return (
<input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
)
}
// 组件 B:只关心 lastName
function LastNameInput() {
const [lastName, setLastName] = useAtom(lastNameAtom)
return (
<input value={lastName} onChange={(e) => setLastName(e.target.value)} />
)
}
// 组件 C:关心全名
function FullNameDisplay() {
const fullName = useAtomValue(fullNameAtom)
// 只有 fullNameAtom 变化时才重新渲染
// 而 fullNameAtom 只会在 firstName 或 lastName 变化时才变化
return <p>全名:{fullName}</p>
}
这是 Jotai 的核心优势——渲染粒度精确到原子级别,没有额外的选择器或 shallow compare。
4. 三大方案对比
| 维度 | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|
| 体积 | ~11KB gzip | ~1KB gzip | ~3KB gzip |
| 架构 | 集中式 Store | 分离式 Store | 原子化 |
| 学习曲线 | 中高 | 低 | 中 |
| TypeScript | 良好 | 优秀 | 优秀 |
| Devtools | 最佳 | 良好(中间件) | 良好 |
| 渲染优化 | useSelector + shallowEqual | Selector 精确订阅 | 原子级自动 |
| 异步处理 | createAsyncThunk | 内置 async 支持 | 异步原子 |
| 中间件 | 丰富的官方生态 | 中间件 API | Jotai 插件系统 |
| 适用场景 | 大型企业级应用 | 中小型应用 | 需要细粒度性能 |
实战案例:电商应用状态管理
让我们用三种方案分别实现一个电商应用的核心状态管理:
场景:购物车 + 产品浏览 + 用户状态
Zustand 实现(简洁优先)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// stores/useEcommerceStore.js
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
const useEcommerceStore = create(
devtools(
persist(
(set, get) => ({
// ── 产品 ──
products: [],
currentProduct: null,
isLoading: false,
setProducts: (products) => set({ products }),
setCurrentProduct: (product) => set({ currentProduct: product }),
fetchProducts: async (category) => {
set({ isLoading: true })
try {
const res = await fetch(`/api/products?category=${category}`)
const data = await res.json()
set({ products: data, isLoading: false })
} catch (err) {
set({ error: err.message, isLoading: false })
}
},
// ── 购物车 ──
cart: [],
cartOpen: false,
toggleCart: () => set((s) => ({ cartOpen: !s.cartOpen })),
addToCart: (product) =>
set((state) => {
const existing = state.cart.find((i) => i.id === product.id)
if (existing) {
return {
cart: state.cart.map((i) =>
i.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
),
}
}
return { cart: [...state.cart, { ...product, quantity: 1 }] }
}),
removeFromCart: (productId) =>
set((state) => ({
cart: state.cart.filter((i) => i.id !== productId),
})),
updateQuantity: (productId, quantity) =>
set((state) => ({
cart: quantity <= 0
? state.cart.filter((i) => i.id !== productId)
: state.cart.map((i) =>
i.id === productId ? { ...i, quantity } : i
),
})),
cartTotal: () =>
get().cart.reduce((sum, i) => sum + i.price * i.quantity, 0),
clearCart: () => set({ cart: [] }),
}),
{
name: 'ecommerce-storage',
partialize: (state) => ({ cart: state.cart }),
}
),
{ name: 'EcommerceStore' }
)
)
export default useEcommerceStore
Jotai 实现(精确渲染)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// atoms/index.js - Jotai 原子实现
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
// 基础原子
export const productsAtom = atom([])
export const currentCategoryAtom = atom('all')
export const currentProductIdAtom = atom(null)
export const isLoadingAtom = atom(false)
// 派生原子:获取当前产品详情
export const currentProductAtom = atom((get) => {
const products = get(productsAtom)
const id = get(currentProductIdAtom)
return products.find((p) => p.id === id) || null
})
// 筛选后的产品列表
export const filteredProductsAtom = atom((get) => {
const products = get(productsAtom)
const category = get(currentCategoryAtom)
if (category === 'all') return products
return products.filter((p) => p.category === category)
})
// 购物车:使用 atomWithStorage 自动持久化
export const cartAtom = atomWithStorage('cart', [])
// 购物车派生计算
export const cartStatsAtom = atom((get) => {
const cart = get(cartAtom)
return {
count: cart.reduce((sum, i) => sum + i.quantity, 0),
subtotal: cart.reduce((sum, i) => sum + i.price * i.quantity, 0),
items: cart,
}
})
// 购物车操作原子
export const addToCartAtom = atom(null, (get, set, product) => {
const cart = get(cartAtom)
const existing = cart.find((i) => i.id === product.id)
if (existing) {
set(cartAtom, cart.map((i) =>
i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
))
} else {
set(cartAtom, [...cart, { ...product, quantity: 1 }])
}
})
export const removeFromCartAtom = atom(null, (get, set, productId) => {
set(cartAtom, get(cartAtom).filter((i) => i.id !== productId))
})
// 异步原子:获取产品列表
export const fetchProductsAtom = atom(
null,
async (get, set, category) => {
set(isLoadingAtom, true)
const data = await fetch(`/api/products?category=${category}`).then((r) => r.json())
set(productsAtom, data)
set(isLoadingAtom, false)
}
)
底层原理(含源码分析)
1. Redux 的发布订阅模型
Redux 的核心是一个非常简单的发布订阅(Pub/Sub)模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Redux 源码核心简化
function createStore(reducer, preloadedState) {
let state = preloadedState
const listeners = new Set()
// dispatch:分发 action
function dispatch(action) {
// 1. 执行 reducer 产生新 state
state = reducer(state, action)
// 2. 通知所有订阅者
listeners.forEach((listener) => listener())
return action
}
// subscribe:订阅变化
function subscribe(listener) {
listeners.add(listener)
// 返回 unsubscribe 函数
return () => listeners.delete(listener)
}
// getState:获取当前状态
function getState() {
return state
}
dispatch({ type: '@@INIT' })
return { dispatch, subscribe, getState }
}
2. Zustand 的 selector 订阅机制
Zustand 的核心是一个可订阅的状态容器,通过 selector 实现精细化的订阅:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Zustand 源码核心简化
function createStore(createState) {
let state
const listeners = new Set()
// 核心:创建状态
const setState = (partial) => {
const nextState = typeof partial === 'function'
? partial(state)
: partial
if (!Object.is(nextState, state)) {
const previousState = state
state = Object.assign({}, state, nextState)
// 通知所有 listener
listeners.forEach((listener) => listener(state, previousState))
}
}
const getState = () => state
const subscribe = (listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
}
const api = { setState, getState, subscribe }
// 初始化 state
state = createState(setState, getState, api)
return api
}
// React 绑定:useStore Hook
function useStore(store, selector = (s) => s) {
const [state, setState] = useState(() => selector(store.getState()))
useEffect(() => {
// 订阅 store 变化
const unsubscribe = store.subscribe((newState) => {
// 使用 selector 提取需要的值
const selected = selector(newState)
// 只有选择的片段变化时才更新
setState(selected)
})
return unsubscribe
}, [store])
return state
}
核心精妙之处:React 渲染是由 setState 触发的。通过在 subscriber 内部使用 setState 而不是通过完整的 store 刷新,Zustand 实现了「selector 级别的精确渲染」——只有组件选择的状态片段变化时才会重新渲染。
3. Jotai 的原子依赖图
Jotai 的核心是构建一个原子依赖图,每个原子都知道它依赖哪些原子,以及被哪些原子依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Jotai 源码核心简化
function atom(read, write) {
const key = Symbol('atom')
return {
key,
read, // (get) => value
write, // (get, set, value) => void
// 运行时会被赋予更多属性
}
}
// 原子状态存储
class AtomStore {
constructor() {
this.atomStateMap = new Map()
this.listeners = new Set()
}
// 读取原子值
get(atom) {
let state = this.atomStateMap.get(atom.key)
if (!state) {
// 首次读取:初始化原子
const value = atom.read({
get: (dep) => this.get(dep) // 递归获取依赖原子
})
state = { value, deps: new Set() }
this.atomStateMap.set(atom.key, state)
}
return state.value
}
// 设置原子值
set(atom, value) {
const state = this.atomStateMap.get(atom.key)
if (typeof value === 'function') {
value = value(state.value)
}
if (!Object.is(state.value, value)) {
state.value = value
// 通知订阅者
this.listeners.forEach((listener) => listener(atom, value))
}
}
}
Jotai 的依赖追踪确保只更新需要更新的组件。当 firstNameAtom 变化时,依赖图如下:
1
2
3
4
5
6
firstNameAtom ──┐
├── fullNameAtom ──→ FullNameDisplay (重新渲染)
lastNameAtom ───┘
FirstNameInput ←── firstNameAtom (重新渲染)
// lastNameAtom 没有变化,LastNameInput 不会重新渲染
高频面试题解析
Q1: Redux 的 useSelector 是如何避免不必要的渲染的?
考察点:React-Redux 的渲染优化机制。
答案核心:
useSelector 内部维护了一个「选择后的值」的引用。每次 store 状态变化时,它会执行选择器函数获取新的值,然后用 === 比较新旧值。如果值相同,就不会触发组件重新渲染。
1
2
3
4
5
6
7
8
9
10
11
12
// useSelector 的核心逻辑
function useSelector(selector) {
const store = useStore()
// 每次 store 变化时执行选择器
const selectedState = useSyncExternalStore(
store.subscribe,
() => selector(store.getState())
)
return selectedState
}
通过 useSyncExternalStore(React 18 内置),React-Redux 能够精确检测 store 变化并仅更新相关组件。此外,shallowEqual 或 equalityFn 选项允许开发者自定义比较逻辑,进一步减少不必要的渲染。
Q2: Zustand 为什么不使用 Context 而是使用闭包?
考察点:对 React 渲染机制和 Zustand 架构的理解。
答案核心:
Zustand 选择使用模块级闭包而不是 React Context 有几个关键原因:
- 避免 Context 的 Provider 嵌套:Context 需要在 Provider 树中传递,而闭包可以在任何地方直接访问状态
- 消除不必要的重新渲染:Context value 变化会导致所有 Consumer 组件重新渲染,无论它们使用哪部分数据;Zustand 的 selector 机制解决了这个问题
- 脱离 React 组件树:Zustand 的状态可以在任何 JavaScript 环境中使用(如纯函数、工具函数),不需要 React 环境
- 性能优势:闭包直接引用内存对象,没有 React reconciler 的介入
1
2
3
4
5
6
7
8
// Context 的问题
// 当 store 变化时,所有消费该 Context 的组件都会重新渲染,
// 即使它们只关心 store 中的某一部分
// Zustand 的优势
// 每个组件精确订阅它选择的状态片段
// 状态可以在 React 外部访问
store.getState() // ✅ 任何地方都能访问
Q3: Jotai 的衍生原子在性能上有什么优势?什么时候该用 Jotai 而不是 Zustand?
考察点:对原子化状态管理性能特性的理解。
答案核心:
Jotai 的性能优势:
- 没有选择器开销:组件直接订阅原子,不需要 selector 函数
- 精确的重渲染边界:原子变化只会影响订阅该原子的组件,不会影响其兄弟组件
- 惰性计算:只有被读取的原子才会计算,未被读取的衍生原子不会执行
选型建议:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用 Jotai 当...
- 状态之间有复杂的依赖关系
- 需要精确控制渲染粒度
- 数据流灵活变化
用 Zustand 当...
- 状态结构相对稳定
- 需要简洁 API 和低学习成本
- SSR/Next.js 场景
- 需要跨组件作用域访问状态
用 Redux Toolkit 当...
- 大型团队协作
- 需要中间件和插件系统
- 复杂的数据获取逻辑(RTK Query)
- 时间旅行调试是刚需
总结与扩展
React 状态管理的生态正在走向「按需选择」的多元化时代,不再有一个方案能统治所有场景。Redux Toolkit 是大型团队和企业级应用的稳妥之选,Zustand 提供了最简洁的体验和最小的体积,Jotai 则为极致渲染性能提供了原子化的解决方案。
核心建议:
- 学习投入:Redux Toolkit 值得投入时间学习,它是 React 生态中最成熟的状态管理方案
- 项目节奏:快速原型用 Zustand,项目扩展时再考虑是否迁移
- 性能敏感:状态变化频繁且需要精确渲染控制的场景,Jotai 是最佳选择
扩展方向:
Server State 与 Client State 的分离是当前趋势。越来越多的项目采用「Zustand/Jotai 管理客户端状态 + TanStack Query 管理服务端状态」的组合方案。这种职责分离模式,让状态管理工具专注于它最擅长的部分——管理「本地、同步」的客户端状态,而数据获取的缓存、加载、错误处理等逻辑则交给专用工具。