为什么写这篇:Redux 是前端状态管理绕不开的"经典"——即使 Zustand、Jotai、Recoil 已流行,Redux 仍是中后台项目、企业级应用的首选。Redux Toolkit(RTK)2019 年发布后,Redux 写法从"百行模板"压缩到"几十行 slice"。2024 年新项目都用 RTK,不要再写裸 Redux。
适用读者:第一次接触 Redux 想搞清全貌的同学;要在 React 项目里集成 Redux 的工程师;维护老 Redux 代码想升级到 RTK 的人。
前置知识:React Hooks 基础;ES6 语法。
目录
- 三件套的"职责分工"
- Redux 核心:store + action + reducer
- React-Redux:连接 React 与 Redux
- Redux Toolkit:告别模板代码
- createSlice:一个文件搞定 action + reducer
- createAsyncThunk:异步 action 标准化
- RTK Query:内置的数据请求与缓存
- 选型对比:Redux vs Zustand vs Jotai
- 项目实战:用户登录状态管理
1. 三件套的"职责分工"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| ┌──────────────────────────────────────────────┐
│ Redux │
│ - 状态管理的"规范"和"核心" │
│ - 提供 store / action / reducer 概念 │
│ - 不绑定任何 UI 框架(React / Vue / 原生 JS) │
└──────────────────────────────────────────────┘
↓ 适配层
┌──────────────────────────────────────────────┐
│ React-Redux │
│ - Redux 与 React 的"绑定层" │
│ - 提供 <Provider> / useSelector / useDispatch │
│ - 负责两者的通信(订阅 store、派发 action) │
└──────────────────────────────────────────────┘
↓ 上层工具
┌──────────────────────────────────────────────┐
│ Redux Toolkit (RTK) │
│ - 基于 Redux 的"官方推荐"封装 │
│ - 内置 immer(直接改 state) │
│ - 内置 thunk(异步 action) │
│ - createSlice 把 action+reducer 写到一起 │
│ - RTK Query:内置数据请求 + 缓存 │
└──────────────────────────────────────────────┘
|
通俗类比:
- Redux = “仓库”——规定状态怎么存、怎么改
- React-Redux = “仓库的出入口”——让 React 组件能"取货"和"补货"
- Redux Toolkit = “仓库的自动化系统”——简化搭建、整理、补货操作
协作关系:
- React-Redux 依赖 Redux(没有 Redux 就没有 React-Redux 的用武之地)
- RTK 内部集成 Redux,并无缝配合 React-Redux
- 实际开发:RTK + React-Redux 一起用(RTK 快速搭 store,React-Redux 让 React 接进来)
2. Redux 核心:store + action + reducer
2.1 三大原则
- 单一 store:整个应用只有一个 store
- state 只读:不能直接改 state,必须 dispatch action
- reducer 纯函数:
(state, action) => newState,不能有副作用
2.2 最小裸 Redux(仅作了解,新项目不要这么写)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 1. reducer:定义状态如何变化
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'increment': return { count: state.count + 1 }
case 'decrement': return { count: state.count - 1 }
case 'set': return { count: action.payload }
default: return state
}
}
// 2. store:创建全局状态容器
import { createStore } from 'redux'
const store = createStore(counterReducer)
// 3. 订阅
store.subscribe(() => console.log(store.getState()))
// 4. 派发 action
store.dispatch({ type: 'increment' })
store.dispatch({ type: 'set', payload: 10 })
|
痛点:每个 state slice 都要写:action type 常量 + action creator 函数 + reducer 的 switch case + 处理不可变更新——模板代码爆炸。RTK 就是来消灭这些模板的。
3. React-Redux:连接 React 与 Redux
3.1 Provider
1
2
3
4
5
6
7
8
9
| import { Provider } from 'react-redux'
function App() {
return (
<Provider store={store}>
<RootComponent />
</Provider>
)
}
|
3.2 useSelector:读状态
1
2
3
4
5
6
| import { useSelector } from 'react-redux'
function Counter() {
const count = useSelector(state => state.counter.count)
return <h1>{count}</h1>
}
|
3.3 useDispatch:派发 action
1
2
3
4
5
6
7
8
9
10
11
| import { useDispatch } from 'react-redux'
function Counter() {
const dispatch = useDispatch()
return (
<>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
)
}
|
vs useState:Redux 的状态是全局共享的——多个组件能读、能改。useState 是组件本地状态。
4.1 装包
1
| npm install @reduxjs/toolkit react-redux
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| // store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer
}
})
// 类型推导
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
|
内置默认:thunk 中间件、redux-devtools 集成、serializable check——开箱即用。
4.3 创建 slice
一个 slice = 一个 state 切片(含 state 初始值、reducers、actions)。
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/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const initialState: CounterState = { value: 0 }
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value += 1 // ✅ 直接改!内部用 immer
},
decrement(state) {
state.value -= 1
},
incrementBy(state, action: PayloadAction<number>) {
state.value += action.payload
}
}
})
// 自动生成 action creators
export const { increment, decrement, incrementBy } = counterSlice.actions
export default counterSlice.reducer
|
关键:用了 createSlice,**不再手写 switch 和 action type 常量**——RTK 自动生成。
4.4 组件使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from './store/counterSlice'
import type { RootState } from './store'
function Counter() {
const count = useSelector((state: RootState) => state.counter.value)
const dispatch = useDispatch()
return (
<>
<h1>{count}</h1>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</>
)
}
|
TypeScript 友好:RootState / AppDispatch 类型导出 → IDE 自动补全 + 类型检查。
5. createSlice:一个文件搞定 action + reducer
Before RTK(百行模板):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // actionTypes.ts
export const INCREMENT = 'counter/INCREMENT'
export const DECREMENT = 'counter/DECREMENT'
// actions.ts
export const increment = () => ({ type: INCREMENT })
export const decrement = () => ({ type: DECREMENT })
// reducer.ts
import { INCREMENT, DECREMENT } from './actionTypes'
export default (state = { count: 0 }, action) => {
switch (action.type) {
case INCREMENT: return { count: state.count + 1 }
case DECREMENT: return { count: state.count - 1 }
default: return state
}
}
|
After RTK(30 行):
1
2
3
4
5
6
7
8
9
10
11
12
13
| import { 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 default counterSlice.reducer
|
省掉 4 个文件、70% 模板代码。
6. createAsyncThunk:异步 action 标准化
处理"发请求 → loading → success/error"的异步流程。
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
| import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { fetchUserApi } from './api'
// 1. 异步 thunk
export const fetchUser = createAsyncThunk(
'user/fetchUser',
async (id: string) => {
const response = await fetchUserApi(id)
return response.data
}
)
const userSlice = createSlice({
name: 'user',
initialState: { data: null, loading: false, error: null },
reducers: {},
// 2. 监听 thunk 的三个状态
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true
state.error = null
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false
state.data = action.payload
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false
state.error = action.error.message ?? 'unknown'
})
}
})
|
组件里:
1
2
3
4
5
6
7
8
9
10
11
12
| function UserProfile({ id }: { id: string }) {
const dispatch = useDispatch<AppDispatch>()
const { data, loading, error } = useSelector((s: RootState) => s.user)
useEffect(() => {
dispatch(fetchUser(id))
}, [id])
if (loading) return <Spinner />
if (error) return <Error msg={error} />
return <div>{data?.name}</div>
}
|
7. RTK Query:内置的数据请求与缓存
RTK Query 是 RTK 自带的数据请求库——替代 React Query / SWR。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // store/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User'],
endpoints: (builder) => ({
getUser: builder.query<User, string>({
query: (id) => `users/${id}`,
providesTags: (_r, _e, id) => [{ type: 'User', id }]
}),
updateUser: builder.mutation<User, Partial<User>>({
query: ({ id, ...patch }) => ({
url: `users/${id}`,
method: 'PATCH',
body: patch
}),
invalidatesTags: (_r, _e, { id }) => [{ type: 'User', id }]
})
})
})
export const { useGetUserQuery, useUpdateUserMutation } = api
|
1
2
3
4
5
6
7
| // 组件
function UserProfile({ id }: { id: string }) {
const { data, isLoading, error } = useGetUserQuery(id)
if (isLoading) return <Spinner />
if (error) return <Error />
return <div>{data?.name}</div>
}
|
优势:
- 自动缓存(同一 ID 不重复请求)
- 自动 refetch(窗口聚焦、网络恢复时)
- 自动失效(mutation 成功后缓存自动更新)
- 内置 loading / error 状态
vs React Query:RTK Query 完全够用,且和 Redux 集成更紧。新项目用 RTK Query 就够。
8. 选型对比:Redux vs Zustand vs Jotai
| 维度 | Redux + RTK | Zustand | Jotai |
|---|
| 学习曲线 | 中(概念多) | 低(一个 hook) | 低(atom 概念) |
| 样板代码 | 中(RTK 已大幅简化) | 极少 | 极少 |
| DevTools | 强(time-travel debug) | 强 | 中 |
| TS 支持 | 强 | 强 | 强 |
| 包大小 | 16KB | 3KB | 4KB |
| 异步处理 | createAsyncThunk / RTK Query | 写函数即可 | 写函数即可 |
| SSR | 强(Next.js 官方推荐) | 中 | 弱 |
| 大型项目 | ✅ 适用(结构清晰) | ✅ 适用 | ⚠️ 大量 atom 难管 |
| 中后台 | ✅ 首选 | ✅ 也行 | ⚠️ |
选型建议:
- 大型项目 + 团队协作:Redux + RTK
- 中型项目 + 快速开发:Zustand
- 细粒度响应式 + 性能敏感:Jotai
- 临时替换 useState:useState + useReducer
9. 项目实战:用户登录状态管理
完整 demo:用 RTK 管理 user 模块的登录/登出/profile。
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
| // store/userSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
interface User {
id: string
name: string
token: string
}
interface UserState {
current: User | null
loading: boolean
error: string | null
}
const initialState: UserState = {
current: null,
loading: false,
error: null
}
// 异步:登录
export const login = createAsyncThunk(
'user/login',
async (params: { username: string; password: string }) => {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(params)
})
return res.json() as Promise<User>
}
)
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
logout: (state) => {
state.current = null
},
setName: (state, action: PayloadAction<string>) => {
if (state.current) state.current.name = action.payload
}
},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => { state.loading = true; state.error = null })
.addCase(login.fulfilled, (state, action) => { state.loading = false; state.current = action.payload })
.addCase(login.rejected, (state, action) => { state.loading = false; state.error = action.error.message ?? '登录失败' })
}
})
export const { logout, setName } = userSlice.actions
export default userSlice.reducer
|
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
| // components/LoginForm.tsx
import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from '../store'
import { login } from '../store/userSlice'
function LoginForm() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const dispatch = useDispatch<AppDispatch>()
const { loading, error, current } = useSelector((s: RootState) => s.user)
if (current) return <Navigate to="/dashboard" />
return (
<form onSubmit={(e) => {
e.preventDefault()
dispatch(login({ username, password }))
}}>
<input value={username} onChange={e => setUsername(e.target.value)} />
<input value={password} type="password" onChange={e => setPassword(e.target.value)} />
<button disabled={loading}>{loading ? '登录中...' : '登录'}</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
)
}
|
小结
Redux 三件套 2024 年的使用姿势:
- 永远用 RTK,不要写裸 Redux
- createSlice 一个文件搞定 action + reducer(immer 支持直接改 state)
- createAsyncThunk 处理 loading / success / error 三态
- RTK Query 替代 React Query,缓存、refetch、失效全自动
- TypeScript 配合
RootState / AppDispatch 类型导出
下一步:学 RTK Query 高级特性(optimistic update、polling、invalidation、streaming updates);或换更轻量的 Zustand(3KB 体积、单 hook、中型项目首选)。
参考资料