Redux Toolkit 教程
项目概述
Redux Toolkit 是 Redux 的官方工具集,旨在简化 Redux 开发流程,减少样板代码,并提供一套最佳实践。它集成了 Immer 库用于不可变状态更新,内置了异步处理能力,并提供了 RTK Query 用于数据获取和缓存管理。Redux Toolkit 已经成为 Redux 官方推荐的标准方式,大幅降低了 Redux 的使用复杂度。
- 项目链接:https://github.com/reduxjs/redux-toolkit
- 官方网站:https://redux-toolkit.js.org/
- GitHub Stars:10k+
- 适用环境:React 项目、Redux 状态管理
安装设置
1. 安装 Redux Toolkit
# 安装核心包
npm install @reduxjs/toolkit react-redux
# 或者使用 yarn
yarn add @reduxjs/toolkit react-redux
# 或者使用 pnpm
pnpm add @reduxjs/toolkit react-redux2. 基本配置
创建 Redux Store
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: {
// 在这里添加你的 reducer
}
})提供 Redux Store
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './app/store'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)核心功能
1. 创建 Slice
Slice 是 Redux Toolkit 中的核心概念,它包含了 reducer 逻辑和 action 创建器。
// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
value: 0,
status: 'idle'
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
// Redux Toolkit 允许我们在 reducers 中直接修改 state
// 这是因为它使用了 Immer 库,会自动将这些修改转换为不可变更新
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
// 导出 action 创建器
export const { increment, decrement, incrementByAmount } = counterSlice.actions
// 导出 reducer
export default counterSlice.reducer在 Store 中使用 Slice
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer
}
})在组件中使用
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, incrementByAmount } from './features/counter/counterSlice'
function Counter() {
// 从 store 中获取状态
const count = useSelector((state) => state.counter.value)
const dispatch = useDispatch()
return (
<div>
<div>{count}</div>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
)
}
export default Counter2. 异步逻辑处理
Redux Toolkit 内置了 createAsyncThunk 用于处理异步逻辑。
// src/features/users/usersSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { fetchUsers } from './usersAPI'
const initialState = {
users: [],
status: 'idle',
error: null
}
// 创建异步 thunk
export const getUsers = createAsyncThunk(
'users/fetchUsers',
async () => {
const response = await fetchUsers()
return response.data
}
)
export const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// 同步 reducers
},
// 处理异步 action
extraReducers: (builder) => {
builder
.addCase(getUsers.pending, (state) => {
state.status = 'loading'
})
.addCase(getUsers.fulfilled, (state, action) => {
state.status = 'succeeded'
state.users = action.payload
})
.addCase(getUsers.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
}
})
export default usersSlice.reducer在组件中使用
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { getUsers } from './features/users/usersSlice'
function UsersList() {
const dispatch = useDispatch()
const { users, status, error } = useSelector((state) => state.users)
useEffect(() => {
if (status === 'idle') {
dispatch(getUsers())
}
}, [status, dispatch])
let content
if (status === 'loading') {
content = <div>加载中...</div>
} else if (status === 'succeeded') {
content = (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
} else if (status === 'failed') {
content = <div>错误: {error}</div>
}
return <div>{content}</div>
}
export default UsersList3. RTK Query
RTK Query 是 Redux Toolkit 中的数据获取和缓存工具。
创建 API 服务
// src/services/api.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// 创建 API 服务
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts'
}),
getPostById: builder.query({
query: (id) => `/posts/${id}`
}),
createPost: builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post
})
})
})
})
// 导出 hooks
export const { useGetPostsQuery, useGetPostByIdQuery, useCreatePostMutation } = api在 Store 中配置
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import { api } from '../services/api'
import counterReducer from '../features/counter/counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
// 添加 API reducer
[api.reducerPath]: api.reducer
},
// 添加 API 中间件
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)
})在组件中使用
import React from 'react'
import { useGetPostsQuery, useCreatePostMutation } from './services/api'
function PostsList() {
// 使用查询 hook
const { data: posts, isLoading, error } = useGetPostsQuery()
const [createPost, { isLoading: isCreating }] = useCreatePostMutation()
const handleCreatePost = async () => {
await createPost({ title: '新文章', body: '文章内容' })
}
if (isLoading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
return (
<div>
<button onClick={handleCreatePost} disabled={isCreating}>
{isCreating ? '创建中...' : '创建文章'}
</button>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
export default PostsList4. 配置 Redux DevTools
Redux Toolkit 默认集成了 Redux DevTools,无需额外配置。
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
// DevTools 已默认启用
export const store = configureStore({
reducer: {
// reducers
}
})
// 可以通过 devTools 选项自定义
// export const store = configureStore({
// reducer: {},
// devTools: process.env.NODE_ENV !== 'production'
// })5. 不可变更新
Redux Toolkit 使用 Immer 库,允许我们在 reducers 中直接修改状态,而 Immer 会自动将这些修改转换为不可变更新。
// src/features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
todos: []
}
export const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action) => {
// 直接修改 state
state.todos.push(action.payload)
},
toggleTodo: (state, action) => {
// 直接修改嵌套对象
const todo = state.todos.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
},
updateTodoText: (state, action) => {
const { id, text } = action.payload
const todo = state.todos.find(todo => todo.id === id)
if (todo) {
todo.text = text
}
}
}
})
export const { addTodo, toggleTodo, updateTodoText } = todosSlice.actions
export default todosSlice.reducer高级功能
1. 自定义 Middleware
// src/app/middleware.js
const loggerMiddleware = (store) => (next) => (action) => {
console.log('Dispatching:', action)
const result = next(action)
console.log('New state:', store.getState())
return result
}
export default loggerMiddleware
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import loggerMiddleware from './middleware'
export const store = configureStore({
reducer: {
// reducers
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware)
})2. 组合 Reducers
// src/features/index.js
import { combineReducers } from '@reduxjs/toolkit'
import counterReducer from './counter/counterSlice'
import todosReducer from './todos/todosSlice'
import usersReducer from './users/usersSlice'
const rootReducer = combineReducers({
counter: counterReducer,
todos: todosReducer,
users: usersReducer
})
export default rootReducer
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from '../features'
export const store = configureStore({
reducer: rootReducer
})3. 持久化状态
# 安装持久化库
npm install redux-persist// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // 默认使用 localStorage
import counterReducer from '../features/counter/counterSlice'
// 持久化配置
const persistConfig = {
key: 'root',
storage
}
// 创建持久化 reducer
const persistedReducer = persistReducer(persistConfig, counterReducer)
export const store = configureStore({
reducer: {
counter: persistedReducer
}
})
// 创建持久化 store
export const persistor = persistStore(store)// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import { store, persistor } from './app/store'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
</React.StrictMode>
)4. 代码分割
// src/features/largeFeature/largeFeatureSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
data: []
}
export const largeFeatureSlice = createSlice({
name: 'largeFeature',
initialState,
reducers: {
// reducers
}
})
export const { actions } = largeFeatureSlice
export default largeFeatureSlice.reducer
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
const store = configureStore({
reducer: {
counter: counterReducer
// 大型特性的 reducer 将通过代码分割动态添加
}
})
// 动态添加 reducer 的方法
export const injectReducer = (key, reducer) => {
if (store.asyncReducers && store.asyncReducers[key]) {
return
}
if (!store.asyncReducers) {
store.asyncReducers = {}
}
store.asyncReducers[key] = reducer
store.replaceReducer({
...store.getState(),
[key]: reducer
})
}
export default store实际应用场景
1. 计数器应用
// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
value: 0,
step: 1
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += state.step
},
decrement: (state) => {
state.value -= state.step
},
setStep: (state, action) => {
state.step = action.payload
},
reset: (state) => {
state.value = 0
}
}
})
export const { increment, decrement, setStep, reset } = counterSlice.actions
export default counterSlice.reducerimport React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, setStep, reset } from './features/counter/counterSlice'
function Counter() {
const { value, step } = useSelector((state) => state.counter)
const dispatch = useDispatch()
const [newStep, setNewStep] = useState(step)
const handleSetStep = () => {
dispatch(setStep(Number(newStep)))
}
return (
<div>
<h1>计数器: {value}</h1>
<div>
<label>步长: </label>
<input
type="number"
value={newStep}
onChange={(e) => setNewStep(e.target.value)}
/>
<button onClick={handleSetStep}>设置步长</button>
</div>
<div>
<button onClick={() => dispatch(increment())}>+{step}</button>
<button onClick={() => dispatch(decrement())}>-{step}</button>
<button onClick={() => dispatch(reset())}>重置</button>
</div>
</div>
)
}
export default Counter2. 待办事项应用
// src/features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { v4 as uuidv4 } from 'uuid'
const initialState = {
todos: []
}
export const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action) => {
state.todos.push({
id: uuidv4(),
text: action.payload,
completed: false
})
},
toggleTodo: (state, action) => {
const todo = state.todos.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
},
removeTodo: (state, action) => {
state.todos = state.todos.filter(todo => todo.id !== action.payload)
},
updateTodo: (state, action) => {
const { id, text } = action.payload
const todo = state.todos.find(todo => todo.id === id)
if (todo) {
todo.text = text
}
}
}
})
export const { addTodo, toggleTodo, removeTodo, updateTodo } = todosSlice.actions
export default todosSlice.reducerimport React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { addTodo, toggleTodo, removeTodo, updateTodo } from './features/todos/todosSlice'
function TodoList() {
const todos = useSelector((state) => state.todos.todos)
const dispatch = useDispatch()
const [inputText, setInputText] = useState('')
const handleAddTodo = () => {
if (inputText.trim()) {
dispatch(addTodo(inputText))
setInputText('')
}
}
return (
<div>
<h1>待办事项</h1>
<div>
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="添加新待办事项"
/>
<button onClick={handleAddTodo}>添加</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<span onClick={() => dispatch(toggleTodo(todo.id))}>{todo.text}</span>
<button onClick={() => dispatch(removeTodo(todo.id))}>删除</button>
</li>
))}
</ul>
</div>
)
}
export default TodoList3. 异步数据获取
// src/features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
// 模拟 API 调用
const fetchPosts = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
return response.json()
}
const fetchPostById = async (id) => {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
return response.json()
}
// 异步 thunks
export const getPosts = createAsyncThunk('posts/fetchPosts', fetchPosts)
export const getPostById = createAsyncThunk('posts/fetchPostById', fetchPostById)
const initialState = {
posts: [],
currentPost: null,
status: 'idle',
error: null
}
export const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
// 获取所有帖子
.addCase(getPosts.pending, (state) => {
state.status = 'loading'
})
.addCase(getPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
state.posts = action.payload
})
.addCase(getPosts.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
// 获取单个帖子
.addCase(getPostById.pending, (state) => {
state.status = 'loading'
})
.addCase(getPostById.fulfilled, (state, action) => {
state.status = 'succeeded'
state.currentPost = action.payload
})
.addCase(getPostById.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
}
})
export default postsSlice.reducerimport React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { getPosts, getPostById } from './features/posts/postsSlice'
function PostsApp() {
const dispatch = useDispatch()
const { posts, currentPost, status, error } = useSelector((state) => state.posts)
useEffect(() => {
dispatch(getPosts())
}, [dispatch])
const handleViewPost = (id) => {
dispatch(getPostById(id))
}
return (
<div>
<h1>帖子列表</h1>
{status === 'loading' && <div>加载中...</div>}
{status === 'failed' && <div>错误: {error}</div>}
{status === 'succeeded' && (
<div>
<div>
<h2>所有帖子</h2>
<ul>
{posts.slice(0, 10).map((post) => (
<li key={post.id}>
<h3>{post.title}</h3>
<button onClick={() => handleViewPost(post.id)}>查看详情</button>
</li>
))}
</ul>
</div>
{currentPost && (
<div>
<h2>帖子详情</h2>
<h3>{currentPost.title}</h3>
<p>{currentPost.body}</p>
</div>
)}
</div>
)}
</div>
)
}
export default PostsApp代码优化建议
文件结构组织:
- 按功能模块组织文件,每个功能模块包含自己的 slice、组件和 API
- 使用
features目录存放功能模块,app目录存放全局配置
使用 createSelector:
- 对于复杂的状态选择逻辑,使用
createSelector进行记忆化,提高性能
import { createSelector } from '@reduxjs/toolkit' const selectTodos = (state) => state.todos.todos export const selectCompletedTodos = createSelector( [selectTodos], (todos) => todos.filter(todo => todo.completed) )- 对于复杂的状态选择逻辑,使用
合理使用 RTK Query:
- 对于数据获取场景,优先使用 RTK Query 而非手动处理异步逻辑
- 利用 RTK Query 的缓存机制减少不必要的网络请求
避免过度使用 Redux:
- 对于组件内部状态,使用 React 的 useState 和 useReducer
- 对于跨组件共享状态,使用 Redux
优化 reducer 逻辑:
- 保持 reducer 逻辑简单纯粹
- 对于复杂业务逻辑,考虑使用自定义 middleware
使用 TypeScript:
- 为 Redux state、actions 和 selectors 添加类型定义
- 提高代码可维护性和类型安全性
测试:
- 为 reducers 和 actions 编写单元测试
- 测试异步 thunks 的各种状态
性能优化:
- 使用
useSelector的依赖数组优化重渲染 - 对于大型应用,考虑使用
createSelector和代码分割
- 使用