Featured image of post Redux 三件套:Redux + React-Redux + Redux Toolkit 实战

Redux 三件套:Redux + React-Redux + Redux Toolkit 实战

Redux 是 JavaScript 生态最经典的状态管理库,2024 年仍被中后台项目广泛使用。本文讲清 Redux、React-Redux、Redux Toolkit 三者的关系与协作——为什么 RTK 是「事实标准」、如何用 createSlice 替代 action + reducer 模板代码、RTK Query 替代 React Query 的可能性。

为什么写这篇:Redux 是前端状态管理绕不开的"经典"——即使 Zustand、Jotai、Recoil 已流行,Redux 仍是中后台项目、企业级应用的首选。Redux Toolkit(RTK)2019 年发布后,Redux 写法从"百行模板"压缩到"几十行 slice"。2024 年新项目都用 RTK,不要再写裸 Redux

适用读者:第一次接触 Redux 想搞清全貌的同学;要在 React 项目里集成 Redux 的工程师;维护老 Redux 代码想升级到 RTK 的人。

前置知识:React Hooks 基础;ES6 语法。

目录

  1. 三件套的"职责分工"
  2. Redux 核心:store + action + reducer
  3. React-Redux:连接 React 与 Redux
  4. Redux Toolkit:告别模板代码
  5. createSlice:一个文件搞定 action + reducer
  6. createAsyncThunk:异步 action 标准化
  7. RTK Query:内置的数据请求与缓存
  8. 选型对比:Redux vs Zustand vs Jotai
  9. 项目实战:用户登录状态管理

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 三大原则

  1. 单一 store:整个应用只有一个 store
  2. state 只读:不能直接改 state,必须 dispatch action
  3. 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. Redux Toolkit:告别模板代码

4.1 装包

1
npm install @reduxjs/toolkit react-redux

4.2 configureStore:替代 createStore

 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,**不再手写 switchaction 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 + RTKZustandJotai
学习曲线中(概念多)低(一个 hook)低(atom 概念)
样板代码中(RTK 已大幅简化)极少极少
DevTools强(time-travel debug)
TS 支持
包大小16KB3KB4KB
异步处理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 年的使用姿势:

  1. 永远用 RTK,不要写裸 Redux
  2. createSlice 一个文件搞定 action + reducer(immer 支持直接改 state)
  3. createAsyncThunk 处理 loading / success / error 三态
  4. RTK Query 替代 React Query,缓存、refetch、失效全自动
  5. TypeScript 配合 RootState / AppDispatch 类型导出

下一步:学 RTK Query 高级特性(optimistic update、polling、invalidation、streaming updates);或换更轻量的 Zustand(3KB 体积、单 hook、中型项目首选)。

参考资料

使用 Hugo 构建
主题 StackJimmy 设计