Appearance
Redux:状态管理模型、数据流与工程实践
很多人一提 React 状态管理,就会马上想到 Redux。
但更自然的问题其实应该是:
- 为什么本地 state 和 Context 还不够
- Redux 到底在解决什么问题
- 它的单向数据流和 React 自己的单向数据流有什么关系
- 什么时候应该用,什么时候不该用
这篇文章重点讲:
- Redux 为什么会出现
store / state / action / reducer各自是什么- 数据更新链路怎么走
- Redux Toolkit 为什么会成为更常见写法
- Redux 的适用边界是什么
1. Redux 为什么会出现
当 React 应用变复杂之后,只靠组件本地 state 往往会遇到这些问题:
- 状态跨很多页面共享
- 组件层级很深,状态传递很痛苦
- 更新链路越来越难追
- 同一块业务状态被很多地方改
Redux 出现,就是为了回答一个更严格的问题:
如果应用里有一批关键共享状态,能不能把“状态放哪、谁能改、怎么改”这几件事统一起来。
2. Redux 的核心模型是什么
Redux 最值得先记住的 4 个词是:
storestateactionreducer
| 概念 | 作用 |
|---|---|
store | 存放全局状态的容器 |
state | 当前应用状态快照 |
action | 一次状态变更意图 |
reducer | 根据旧状态和 action 计算新状态 |
🌟 把这句话记住:Redux 强调的不是“多一个库”,而是把状态变更写成可追踪的显式数据流。
3. Redux 的单向数据流怎么走
Redux 的主线可以压成这样:
text
UI 触发 action -> reducer 计算新 state -> store 保存新 state -> UI 重新读取并渲染也就是说:
- 页面不会直接随便改状态
- 必须先发起一个 action
- reducer 决定如何从旧状态算出新状态
这条链路很适合复杂应用,因为状态变化来源更清楚。
4. 一个最小 Redux 示例
js
import { configureStore, createSlice } 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: {
counter: counterSlice.reducer
}
});配合 React:
jsx
import { Provider, useDispatch, useSelector } from "react-redux";
import { decrement, increment, store } from "./store";
function Counter() {
const value = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{value}</span>
<button onClick={() => dispatch(increment())}>+</button>
</>
);
}
export default function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}这段代码最值得注意的是:
- 组件不直接改全局状态
- 组件通过
dispatch发 action - 组件通过
useSelector读取 store 中自己关心的片段
5. 为什么现在更常讲 Redux Toolkit
如果你只学过早期 Redux,可能会觉得它样板代码很多。
例如以前很常见的问题是:
- action type 常量很多
- action creator 很分散
- reducer 模板代码偏重
- store 配置较繁琐
Redux Toolkit 主要就是为了解决这件事。
它更常见的价值包括:
- 用
createSlice把 action 和 reducer 收到一起 - 用
configureStore简化 store 配置 - 默认集成常见中间件和开发体验
所以工程上更常见的说法通常不是“Redux vs Redux Toolkit”,而是 现代 Redux 实践通常就是 Redux Toolkit。
6. Redux 和 Context 有什么区别
这两个词很容易被放在一起比较。
| 能力 | 更适合什么 |
|---|---|
| Context | 共享上下文,例如主题、用户、语言 |
| Redux | 复杂共享业务状态和明确更新链路 |
Context 更适合:
- 跨层共享
- 语义偏上下文
- 更新逻辑不算特别复杂
Redux 更适合:
- 状态规模更大
- 更新动作更多
- 需要更明确追踪状态变更
- 多个页面和模块共用一套核心数据
7. 什么时候适合上 Redux
比较适合的场景通常包括:
- 登录信息、权限、菜单、工作台全局会话态
- 购物车、筛选条件、分页信息等跨模块共享状态
- 更新逻辑复杂,且来源很多
- 团队需要更稳定的状态管理约束
不太适合的场景通常是:
- 只在一个组件里用到的状态
- 临时输入框值
- 简单弹窗开关
- 很容易通过状态提升解决的问题
🌟 一个很重要的判断是,不是“用了 React 就该有 Redux”,而是“全局共享状态真的复杂到值得统一建模时,再上 Redux”。
8. Redux 常见的工程关注点
8.1 状态切片怎么拆
更自然的拆法通常按业务域或页面域来拆,而不是机械按类型拆。
8.2 异步逻辑放哪
现代 Redux 实践里,常见会放在:
- thunk
- RTK Query
- 独立数据请求层
8.3 不是所有服务端数据都该进 Redux
如果数据更像“远程缓存”,很多时候更适合用专门的数据获取库,而不一定非要全进全局 store。
8.4 Redux 和 @tanstack/react-query 是什么关系
这几年很多 React 项目里,大家讨论状态管理时,已经不会只在 Context 和 Redux 之间二选一了。
因为还有一类问题并不适合直接按“全局业务状态”来建模,它更像:
- 服务器返回的数据怎么拉取
- 数据什么时候失效
- 切页面后数据要不要复用
- 请求中的 loading、error、success 状态怎么统一管理
这类问题更常见的方案就是 @tanstack/react-query。
安装通常类似这样:
bash
pnpm add @tanstack/react-query它更像:一个专门管理服务端状态、请求缓存、失效重取和异步请求生命周期的库。
所以把边界放回原位看:
| 工具 | 更偏解决什么 |
|---|---|
| Redux | 客户端共享业务状态、显式更新链路、复杂状态治理 |
@tanstack/react-query | 服务端数据获取、缓存、失效、重试、后台刷新 |
例如下面这些数据,更适合优先考虑 react-query:
- 用户详情
- 商品列表
- 分页结果
- 评论列表
- 订单详情
而下面这些状态,通常仍然更适合 Redux:
- 当前菜单折叠状态
- 多模块共享的筛选面板开关
- 工作台里的复杂本地编辑状态
- 明确需要 action / reducer 约束的客户端业务状态
🌟 要注意的是,服务端数据不是“全局状态仓库里的一块普通对象”,它更像“带缓存、失效时间、刷新策略的远程数据”。
8.5 一个最小 react-query 接入示例
先在应用入口提供 QueryClient:
tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import App from './App'
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
)然后在组件里通过 useQuery 拉取数据:
tsx
import { useQuery } from '@tanstack/react-query'
async function fetchUserList() {
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error('加载用户列表失败')
}
return response.json()
}
export function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUserList,
staleTime: 60_000,
})
if (isLoading) {
return <div>加载中...</div>
}
if (error) {
return <div>加载失败</div>
}
return (
<ul>
{data.map((user: { id: number; name: string }) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}这段示例里最关键的是:
queryKey用来标识这份远程数据queryFn负责真正发请求staleTime用来定义“多久之内这份数据算新鲜”
也就是说,组件不再自己手写一套:
loadingerrordata重复请求去重缓存复用
这些通用异步数据管理逻辑。
8.6 Redux 和 react-query 怎么一起用
工程里很常见的组合不是“二选一”,而是同时存在:
Redux管客户端共享业务状态@tanstack/react-query管服务端数据
例如一个管理后台页面里:
- 表格列表数据、详情数据、分页数据可以交给
react-query - 当前页签、布局开关、选中的本地草稿、复杂联动状态可以交给
Redux
这套分工的好处是:
- 远程数据不用再手写大量异步 action 和缓存逻辑
- Redux 不会被接口数据塞成“大仓库”
- 状态边界会更清楚
8.7 Redux 持久化怎么做
很多业务状态并不只想“页面活着时可用”,而是希望刷新页面后还在。
例如:
- 登录态中的部分信息
- 用户主题配置
- 多页面共享的筛选条件
- 购物车
这时常见做法就是给 Redux 加一层持久化。
工程里更常见的方案是使用 redux-persist。
安装通常类似这样:
bash
pnpm add redux-persist它的核心思路就是:
- 先用正常 reducer 组织业务状态
- 再用
persistReducer(...)包一层需要持久化的 reducer - 把状态写入
localStorage、sessionStorage或自定义存储 - 应用启动时再把已保存状态回灌到 store
下面是一份贴近工程主线的精简示例:
ts
import { combineReducers, configureStore } from '@reduxjs/toolkit'
import {
persistReducer,
persistStore,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import authReducer from './features/auth/authSlice'
import settingsReducer from './features/settings/settingsSlice'
const rootReducer = combineReducers({
auth: authReducer,
settings: settingsReducer,
})
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth', 'settings'],
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
})
export const persistor = persistStore(store)在入口里通常再配合 PersistGate:
tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import App from './App'
import { persistor, store } from './store'
ReactDOM.createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
)这套集成里最重要的 3 个点是:
persistReducer负责把持久化能力包到 reducer 上persistStore负责创建持久化控制器PersistGate负责在回灌完成前延迟渲染,避免页面看到空状态再闪一下
8.8 Redux 持久化时最该注意什么
持久化不是“把整个 store 都存起来”。
更稳的做法通常是只持久化少量关键状态,例如:
- 主题模式
- 登录用户的非敏感展示信息
- 多页共享筛选条件
- 草稿类表单
不建议直接长期持久化的通常包括:
- 体积很大的列表数据
- 明显属于远程缓存的数据
- 一次性 UI 状态
- 高敏感信息,例如明文 token、密钥、密码
🌟 要注意的是,持久化解决的是“刷新后还要不要保留”,不是“所有全局状态都该落盘”。
如果只想保留一部分字段,也可以继续在 slice 层做更细粒度控制,而不是整个根状态一把全存。
9. Redux 最常见的误区
9.1 以为 Redux 就是全局变量
不是。
它更强调的是可预测更新链路。
9.2 以为 Redux 一定让项目更规范
如果状态本来不复杂,Redux 也可能只会增加负担。
9.3 以为所有共享数据都该进 Redux
很多局部状态、表单状态、本地 UI 状态没必要上升到全局。
10. 一句话总结
Redux 的核心价值,不是“多一个状态库”,而是把复杂共享状态的变更过程收口成一条清晰的单向数据流,让状态从“到处可改”变成“有入口、有规则、可追踪”。