Skip to content

Redux:状态管理模型、数据流与工程实践

很多人一提 React 状态管理,就会马上想到 Redux。

但更自然的问题其实应该是:

  1. 为什么本地 state 和 Context 还不够
  2. Redux 到底在解决什么问题
  3. 它的单向数据流和 React 自己的单向数据流有什么关系
  4. 什么时候应该用,什么时候不该用

这篇文章重点讲:

  1. Redux 为什么会出现
  2. store / state / action / reducer 各自是什么
  3. 数据更新链路怎么走
  4. Redux Toolkit 为什么会成为更常见写法
  5. Redux 的适用边界是什么

1. Redux 为什么会出现

当 React 应用变复杂之后,只靠组件本地 state 往往会遇到这些问题:

  1. 状态跨很多页面共享
  2. 组件层级很深,状态传递很痛苦
  3. 更新链路越来越难追
  4. 同一块业务状态被很多地方改

Redux 出现,就是为了回答一个更严格的问题:

如果应用里有一批关键共享状态,能不能把“状态放哪、谁能改、怎么改”这几件事统一起来。


2. Redux 的核心模型是什么

Redux 最值得先记住的 4 个词是:

  1. store
  2. state
  3. action
  4. reducer
概念作用
store存放全局状态的容器
state当前应用状态快照
action一次状态变更意图
reducer根据旧状态和 action 计算新状态

🌟 把这句话记住:Redux 强调的不是“多一个库”,而是把状态变更写成可追踪的显式数据流。


3. Redux 的单向数据流怎么走

Redux 的主线可以压成这样:

text
UI 触发 action -> reducer 计算新 state -> store 保存新 state -> UI 重新读取并渲染

也就是说:

  1. 页面不会直接随便改状态
  2. 必须先发起一个 action
  3. 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>
  );
}

这段代码最值得注意的是:

  1. 组件不直接改全局状态
  2. 组件通过 dispatch 发 action
  3. 组件通过 useSelector 读取 store 中自己关心的片段

5. 为什么现在更常讲 Redux Toolkit

如果你只学过早期 Redux,可能会觉得它样板代码很多。

例如以前很常见的问题是:

  1. action type 常量很多
  2. action creator 很分散
  3. reducer 模板代码偏重
  4. store 配置较繁琐

Redux Toolkit 主要就是为了解决这件事。

它更常见的价值包括:

  1. createSlice 把 action 和 reducer 收到一起
  2. configureStore 简化 store 配置
  3. 默认集成常见中间件和开发体验

所以工程上更常见的说法通常不是“Redux vs Redux Toolkit”,而是 现代 Redux 实践通常就是 Redux Toolkit。


6. Redux 和 Context 有什么区别

这两个词很容易被放在一起比较。

能力更适合什么
Context共享上下文,例如主题、用户、语言
Redux复杂共享业务状态和明确更新链路

Context 更适合:

  1. 跨层共享
  2. 语义偏上下文
  3. 更新逻辑不算特别复杂

Redux 更适合:

  1. 状态规模更大
  2. 更新动作更多
  3. 需要更明确追踪状态变更
  4. 多个页面和模块共用一套核心数据

7. 什么时候适合上 Redux

比较适合的场景通常包括:

  1. 登录信息、权限、菜单、工作台全局会话态
  2. 购物车、筛选条件、分页信息等跨模块共享状态
  3. 更新逻辑复杂,且来源很多
  4. 团队需要更稳定的状态管理约束

不太适合的场景通常是:

  1. 只在一个组件里用到的状态
  2. 临时输入框值
  3. 简单弹窗开关
  4. 很容易通过状态提升解决的问题

🌟 一个很重要的判断是,不是“用了 React 就该有 Redux”,而是“全局共享状态真的复杂到值得统一建模时,再上 Redux”。


8. Redux 常见的工程关注点

8.1 状态切片怎么拆

更自然的拆法通常按业务域或页面域来拆,而不是机械按类型拆。

8.2 异步逻辑放哪

现代 Redux 实践里,常见会放在:

  1. thunk
  2. RTK Query
  3. 独立数据请求层

8.3 不是所有服务端数据都该进 Redux

如果数据更像“远程缓存”,很多时候更适合用专门的数据获取库,而不一定非要全进全局 store。

8.4 Redux 和 @tanstack/react-query 是什么关系

这几年很多 React 项目里,大家讨论状态管理时,已经不会只在 ContextRedux 之间二选一了。

因为还有一类问题并不适合直接按“全局业务状态”来建模,它更像:

  1. 服务器返回的数据怎么拉取
  2. 数据什么时候失效
  3. 切页面后数据要不要复用
  4. 请求中的 loading、error、success 状态怎么统一管理

这类问题更常见的方案就是 @tanstack/react-query

安装通常类似这样:

bash
pnpm add @tanstack/react-query

它更像:一个专门管理服务端状态、请求缓存、失效重取和异步请求生命周期的库。

所以把边界放回原位看:

工具更偏解决什么
Redux客户端共享业务状态、显式更新链路、复杂状态治理
@tanstack/react-query服务端数据获取、缓存、失效、重试、后台刷新

例如下面这些数据,更适合优先考虑 react-query

  1. 用户详情
  2. 商品列表
  3. 分页结果
  4. 评论列表
  5. 订单详情

而下面这些状态,通常仍然更适合 Redux

  1. 当前菜单折叠状态
  2. 多模块共享的筛选面板开关
  3. 工作台里的复杂本地编辑状态
  4. 明确需要 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>
  )
}

这段示例里最关键的是:

  1. queryKey 用来标识这份远程数据
  2. queryFn 负责真正发请求
  3. staleTime 用来定义“多久之内这份数据算新鲜”

也就是说,组件不再自己手写一套:

  1. loading
  2. error
  3. data
  4. 重复请求去重
  5. 缓存复用

这些通用异步数据管理逻辑。

8.6 Redux 和 react-query 怎么一起用

工程里很常见的组合不是“二选一”,而是同时存在:

  1. Redux 管客户端共享业务状态
  2. @tanstack/react-query 管服务端数据

例如一个管理后台页面里:

  1. 表格列表数据、详情数据、分页数据可以交给 react-query
  2. 当前页签、布局开关、选中的本地草稿、复杂联动状态可以交给 Redux

这套分工的好处是:

  1. 远程数据不用再手写大量异步 action 和缓存逻辑
  2. Redux 不会被接口数据塞成“大仓库”
  3. 状态边界会更清楚

8.7 Redux 持久化怎么做

很多业务状态并不只想“页面活着时可用”,而是希望刷新页面后还在。

例如:

  1. 登录态中的部分信息
  2. 用户主题配置
  3. 多页面共享的筛选条件
  4. 购物车

这时常见做法就是给 Redux 加一层持久化。

工程里更常见的方案是使用 redux-persist

安装通常类似这样:

bash
pnpm add redux-persist

它的核心思路就是:

  1. 先用正常 reducer 组织业务状态
  2. 再用 persistReducer(...) 包一层需要持久化的 reducer
  3. 把状态写入 localStoragesessionStorage 或自定义存储
  4. 应用启动时再把已保存状态回灌到 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 个点是:

  1. persistReducer 负责把持久化能力包到 reducer 上
  2. persistStore 负责创建持久化控制器
  3. PersistGate 负责在回灌完成前延迟渲染,避免页面看到空状态再闪一下

8.8 Redux 持久化时最该注意什么

持久化不是“把整个 store 都存起来”。

更稳的做法通常是只持久化少量关键状态,例如:

  1. 主题模式
  2. 登录用户的非敏感展示信息
  3. 多页共享筛选条件
  4. 草稿类表单

不建议直接长期持久化的通常包括:

  1. 体积很大的列表数据
  2. 明显属于远程缓存的数据
  3. 一次性 UI 状态
  4. 高敏感信息,例如明文 token、密钥、密码

🌟 要注意的是,持久化解决的是“刷新后还要不要保留”,不是“所有全局状态都该落盘”。

如果只想保留一部分字段,也可以继续在 slice 层做更细粒度控制,而不是整个根状态一把全存。


9. Redux 最常见的误区

9.1 以为 Redux 就是全局变量

不是。

它更强调的是可预测更新链路。

9.2 以为 Redux 一定让项目更规范

如果状态本来不复杂,Redux 也可能只会增加负担。

9.3 以为所有共享数据都该进 Redux

很多局部状态、表单状态、本地 UI 状态没必要上升到全局。


10. 一句话总结

Redux 的核心价值,不是“多一个状态库”,而是把复杂共享状态的变更过程收口成一条清晰的单向数据流,让状态从“到处可改”变成“有入口、有规则、可追踪”。

基于 VitePress 构建的个人技术笔记。