1. Redux核心概念解析
Redux本质上是一个状态管理容器,它通过单一数据源(store)和严格的单向数据流来管理应用状态。这种架构模式特别适合中大型前端应用,尤其是当多个组件需要共享和同步状态时。
1.1 三大基本原则
单一数据源:整个应用的状态被存储在一个对象树中,这个对象树只存在于唯一的store里。这种设计让调试和检查变得简单,例如时间旅行调试(time travel debugging)成为可能。
状态只读:改变状态的唯一方法是触发action。这种限制确保了状态修改的集中化和可预测性,任何状态变化都会被记录下来。
纯函数修改:使用reducer来描述状态变化。reducer是纯函数,它接收先前的state和action,返回新的state。这种设计让状态变化逻辑可测试且可预测。
1.2 核心工作流程
- 触发action:组件通过dispatch方法发送一个描述"发生了什么"的普通对象
- reducer处理:store调用reducer函数,传入当前state和action
- 状态更新:reducer返回新的state,store更新内部状态
- 通知订阅者:store通知所有订阅了状态变化的组件
- 组件更新:组件通过selector获取需要的状态片段并重新渲染
提示:虽然Redux核心概念简单,但手动实现完整流程需要大量样板代码。这正是Redux Toolkit要解决的问题。
2. Redux Toolkit深度解析
2.1 为什么需要RTK
传统Redux开发面临几个主要痛点:
- 需要手动编写大量样板代码(action types、action creators等)
- 不可变更新容易出错(意外修改原state)
- store配置复杂(中间件、DevTools集成等)
- 代码组织分散(actions、reducers、constants分文件)
Redux Toolkit通过提供一系列工具函数解决了这些问题,它现在是Redux官方推荐的标准写法。
2.2 核心API详解
2.2.1 configureStore
javascript复制import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
devTools: process.env.NODE_ENV !== 'production',
preloadedState: initialState
})
configureStore自动完成了以下工作:
- 组合传入的reducer
- 添加redux-thunk中间件
- 设置Redux DevTools扩展
- 包含开发环境检查(如意外修改state)
2.2.2 createSlice
javascript复制import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => state + 1,
decrement: (state) => state - 1,
addBy: (state, action) => state + action.payload
}
})
export const { increment, decrement, addBy } = counterSlice.actions
export default counterSlice.reducer
createSlice的优势:
- 自动生成action creators
- 自动生成action types
- 使用Immer简化不可变更新
- 将相关逻辑组织在一个文件中
2.2.3 createAsyncThunk
javascript复制import { createAsyncThunk } from '@reduxjs/toolkit'
export const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// 在slice中处理异步状态
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {
state.status = 'loading'
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.status = 'succeeded'
state.data = action.payload
})
.addCase(fetchUserById.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
}
3. Redux DevTools实战指南
3.1 安装与配置
-
浏览器安装扩展:
- Chrome: Redux DevTools扩展
- Firefox: 同款扩展
-
基本配置:
javascript复制const store = configureStore({
reducer,
devTools: {
name: 'MyApp', // 实例名称
trace: true, // 启用action调用栈跟踪
traceLimit: 25 // 调用栈跟踪深度
}
})
3.2 核心功能解析
时间旅行调试:
- 查看完整的action历史记录
- 滑动时间轴回放状态变化
- 导出/导入状态快照
状态检查:
- 实时查看当前state树
- 搜索和过滤state属性
- 显示状态差异(diff)
Action监控:
- 查看每个action的payload
- 显示action触发时间戳
- 查看前后状态变化
3.3 高级使用技巧
- 状态持久化:将特定状态保存到localStorage
- 测试用例生成:基于action历史生成测试用例
- 性能分析:检测不必要的re-render
- 远程调试:通过remotedev.io实现跨设备调试
4. 现代Redux最佳实践
4.1 项目结构组织
推荐的文件结构:
code复制/src
/features
/user
userSlice.js
UserList.js
UserForm.js
/app
store.js
rootReducer.js
关键原则:
- 按功能而非技术角色组织文件
- 将相关UI和状态逻辑放在一起
- 避免过度拆分action/reducer文件
4.2 性能优化策略
- 记忆化selector:
javascript复制import { createSelector } from '@reduxjs/toolkit'
const selectUserState = (state) => state.user
export const selectActiveUsers = createSelector(
[selectUserState],
(user) => user.data.filter(u => u.isActive)
)
- 批量更新:使用RTK的
prepare减少不必要的渲染 - 状态规范化:避免嵌套过深的数据结构
- 惰性加载reducer:动态注入reducer减少初始包大小
4.3 常见问题解决方案
循环依赖:
- 将共享逻辑提取到单独文件
- 使用动态导入
- 重构组件层次结构
状态序列化警告:
- 避免在state中存储非序列化数据(如函数、类实例)
- 使用自定义序列化中间件处理特殊类型
大型状态树:
- 使用RTK的
createEntityAdapter管理标准化数据 - 考虑状态分片(state slicing)
- 评估是否真的需要全局状态
5. 从传统Redux迁移到RTK
5.1 迁移步骤
- 替换createStore:
diff复制- import { createStore } from 'redux'
+ import { configureStore } from '@reduxjs/toolkit'
- 转换reducer为slice:
javascript复制// 传统reducer
function todosReducer(state = [], action) {
switch(action.type) {
case 'ADD_TODO':
return [...state, action.payload]
// ...
}
}
// RTK slice
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload) // 使用Immer
}
}
})
- 更新异步逻辑:
javascript复制// 传统thunk
function fetchUser(userId) {
return (dispatch) => {
dispatch(requestUser())
return api.getUser(userId)
.then(response => dispatch(receiveUser(response)))
}
}
// RTK async thunk
const fetchUser = createAsyncThunk(
'users/fetch',
async (userId) => {
const response = await api.getUser(userId)
return response.data
}
)
5.2 迁移注意事项
- 不可变更新:虽然Immer允许"突变"写法,但只适用于slice内部
- action类型:RTK生成的action类型格式为
sliceName/reducerKey - 中间件顺序:configureStore已包含常用中间件,注意自定义中间件顺序
- TypeScript类型:RTK提供更好的类型推断,可能需要调整类型定义
6. 实战案例:电商购物车实现
6.1 状态设计
javascript复制const initialState = {
items: [],
shippingInfo: null,
discount: 0,
loading: false,
error: null
}
6.2 Slice实现
javascript复制const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem: (state, action) => {
const existingItem = state.items.find(
item => item.id === action.payload.id
)
if(existingItem) {
existingItem.quantity += action.payload.quantity
} else {
state.items.push(action.payload)
}
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload)
},
updateQuantity: (state, action) => {
const item = state.items.find(item => item.id === action.payload.id)
if(item) {
item.quantity = action.payload.quantity
}
}
},
extraReducers: (builder) => {
builder
.addCase(checkout.pending, (state) => {
state.loading = true
})
.addCase(checkout.fulfilled, (state) => {
state.loading = false
state.items = []
})
.addCase(checkout.rejected, (state, action) => {
state.loading = false
state.error = action.error.message
})
}
})
6.3 异步操作处理
javascript复制export const checkout = createAsyncThunk(
'cart/checkout',
async (paymentInfo, { getState, rejectWithValue }) => {
try {
const { cart } = getState()
const response = await api.post('/checkout', {
items: cart.items,
payment: paymentInfo
})
return response.data
} catch (err) {
return rejectWithValue(err.response.data)
}
}
)
6.4 组件集成
javascript复制function Cart() {
const dispatch = useDispatch()
const { items, loading } = useSelector(state => state.cart)
const handleCheckout = () => {
dispatch(checkout(paymentInfo))
}
return (
<div>
{items.map(item => (
<CartItem
key={item.id}
item={item}
onRemove={() => dispatch(removeItem(item.id))}
/>
))}
<button
onClick={handleCheckout}
disabled={loading}
>
{loading ? 'Processing...' : 'Checkout'}
</button>
</div>
)
}
7. 性能优化与调试技巧
7.1 组件渲染优化
- 记忆化selector:
javascript复制const selectCartItems = (state) => state.cart.items
export const selectCartTotal = createSelector(
[selectCartItems],
(items) => items.reduce((total, item) => total + item.price * item.quantity, 0)
)
- 浅比较优化:
javascript复制const CartItem = React.memo(({ item }) => {
// 组件实现
}, (prevProps, nextProps) => {
return prevProps.item.id === nextProps.item.id &&
prevProps.item.quantity === nextProps.item.quantity
})
7.2 状态序列化检查
javascript复制const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['some/non-serializable-action'],
ignoredPaths: ['some.nonSerializable.field']
}
})
})
7.3 调试技巧
- Action追踪:
javascript复制const middleware = storeAPI => next => action => {
console.group(action.type)
console.log('Dispatching:', action)
const result = next(action)
console.log('Next state:', storeAPI.getState())
console.groupEnd()
return result
}
- 状态快照比较:
javascript复制// 在测试中
const state1 = store.getState()
dispatch(someAction())
const state2 = store.getState()
expect(state1).not.toEqual(state2)
8. 常见问题与解决方案
8.1 状态更新但UI不刷新
可能原因:
- 直接修改了state而不是返回新state
- selector没有正确记忆化
- 组件没有正确订阅state变化
解决方案:
- 使用Immer确保不可变更新
- 检查selector依赖项
- 确认组件使用了useSelector
8.2 循环依赖问题
场景:
- Slice A依赖Slice B的action
- Slice B又依赖Slice A的action
解决方案:
- 将共享逻辑提取到单独文件
- 使用
extraReducers响应其他slice的action - 考虑重构状态结构
8.3 大型状态树性能问题
优化策略:
- 使用状态规范化
javascript复制{
users: {
ids: ['user1', 'user2'],
entities: {
'user1': { id: 'user1', name: 'User 1' },
'user2': { id: 'user2', name: 'User 2' }
}
}
}
- 实现分页和懒加载
- 使用
createEntityAdapter管理标准化数据
8.4 异步操作竞态条件
解决方案:
javascript复制const fetchUserData = createAsyncThunk(
'user/fetchData',
async (userId, { getState, requestId }) => {
const { currentRequestId } = getState().user
if(requestId !== currentRequestId) {
return
}
const response = await api.getUser(userId)
return response.data
}
)
// 在slice中
extraReducers: (builder) => {
builder.addCase(fetchUserData.pending, (state, action) => {
state.currentRequestId = action.meta.requestId
})
}
9. 测试策略与实践
9.1 单元测试slice
javascript复制import reducer, { increment } from './counterSlice'
test('should handle increment', () => {
const previousState = { value: 0 }
expect(reducer(previousState, increment())).toEqual(
{ value: 1 }
)
})
9.2 异步thunk测试
javascript复制import { fetchUser } from './userSlice'
jest.mock('./api')
test('should fetch user data', async () => {
const dispatch = jest.fn()
const getState = jest.fn()
const userData = { id: 1, name: 'Test User' }
api.getUser.mockResolvedValue({ data: userData })
await fetchUser(1)(dispatch, getState, undefined)
expect(dispatch).toHaveBeenCalledWith(
fetchUser.pending('', 1)
)
expect(dispatch).toHaveBeenCalledWith(
fetchUser.fulfilled(userData, '', 1)
)
})
9.3 组件集成测试
javascript复制import { render, screen } from '@testing-library/react'
import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'
import Cart from './Cart'
const mockStore = configureStore([])
test('should display cart items', () => {
const store = mockStore({
cart: {
items: [
{ id: 1, name: 'Product 1', quantity: 1 }
]
}
})
render(
<Provider store={store}>
<Cart />
</Provider>
)
expect(screen.getByText('Product 1')).toBeInTheDocument()
})
10. 进阶主题与扩展
10.1 RTK Query深度集成
javascript复制import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getProducts: builder.query({
query: () => 'products'
}),
addProduct: builder.mutation({
query: (product) => ({
url: 'products',
method: 'POST',
body: product
})
})
})
})
export const { useGetProductsQuery, useAddProductMutation } = api
10.2 服务端渲染支持
- 创建store实例:
javascript复制export const makeStore = (preloadedState) => {
return configureStore({
reducer: rootReducer,
preloadedState
})
}
- 在服务端:
javascript复制const store = makeStore(initialState)
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
)
const finalState = store.getState()
- 客户端hydration:
javascript复制const store = makeStore(window.__PRELOADED_STATE__)
hydrateRoot(
document.getElementById('root'),
<Provider store={store}>
<App />
</Provider>
)
10.3 状态持久化方案
javascript复制import { persistReducer, persistStore } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth']
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = configureStore({
reducer: persistedReducer
})
const persistor = persistStore(store)
10.4 微前端集成策略
- 独立store模式:
- 每个微应用有自己的Redux store
- 通过custom event或postMessage通信
- 共享store模式:
javascript复制// 主应用
const store = configureStore({
reducer: {
main: mainReducer,
// 预留微应用reducer位置
}
})
// 微应用
export function registerReducer(store) {
store.injectReducer('microApp', microAppReducer)
}
11. 性能监控与分析
11.1 Redux性能指标
- Action处理时间:
javascript复制const performanceMiddleware = store => next => action => {
const start = performance.now()
const result = next(action)
const end = performance.now()
console.log(`${action.type} took ${end - start}ms`)
return result
}
- Selector重计算次数:
javascript复制const createSelector = defaultMemoize => {
let recomputations = 0
const selector = (...args) => {
recomputations++
return defaultMemoize(...args)
}
selector.recomputations = () => recomputations
return selector
}
11.2 内存使用分析
- 状态快照比较:
javascript复制function trackMemoryUsage() {
const state = store.getState()
const size = JSON.stringify(state).length
console.log(`State size: ${size} bytes`)
}
- 内存泄漏检测:
- 检查未清理的订阅
- 监控store listener数量
- 定期dump state检查异常增长
11.3 生产环境监控
- 错误跟踪:
javascript复制const errorMiddleware = store => next => action => {
try {
return next(action)
} catch (err) {
logErrorToService(err, action)
throw err
}
}
- 性能指标上报:
javascript复制const analyticsMiddleware = store => next => action => {
const start = Date.now()
const result = next(action)
const duration = Date.now() - start
if(duration > 100) {
reportSlowAction(action.type, duration)
}
return result
}
12. 生态系统集成
12.1 与React Router集成
javascript复制import { connectRouter, routerMiddleware } from 'connected-react-router'
const store = configureStore({
reducer: {
router: connectRouter(history),
// ...其他reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(routerMiddleware(history))
})
12.2 与Formik集成
javascript复制function FormWithRedux() {
const dispatch = useDispatch()
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => {
dispatch(submitForm(values))
}}
>
{/* 表单内容 */}
</Formik>
)
}
12.3 与GraphQL集成
javascript复制import { createApi } from '@reduxjs/toolkit/query/react'
import { graphqlRequestBaseQuery } from '@rtk-query/graphql-request-base-query'
const api = createApi({
baseQuery: graphqlRequestBaseQuery({
url: '/graphql',
}),
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => ({
document: gql`
query User($id: ID!) {
user(id: $id) {
id
name
}
}
`,
variables: { id },
}),
}),
}),
})
13. 迁移与升级策略
13.1 从Redux传统模式迁移
- 渐进式迁移路径:
- 先替换createStore为configureStore
- 逐步转换reducer为createSlice
- 最后处理异步逻辑
- 混合模式运行:
javascript复制const legacyReducer = combineReducers({
old: oldReducer
})
const rootReducer = combineReducers({
legacy: legacyReducer,
new: newSlice.reducer
})
13.2 从Redux-Saga迁移
- 转换策略:
- 将saga effect转换为createAsyncThunk
- 复杂流程使用RTK listener middleware
- 保留必要的saga作为过渡
- 混合模式示例:
javascript复制const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(sagaMiddleware)
})
sagaMiddleware.run(rootSaga)
13.3 从MobX迁移
- 概念映射:
- MobX store → Redux slice
- @action → reducer
- @computed → selector
- reaction → listener middleware
- 渐进式方案:
- 在MobX组件中使用Redux store
- 逐步重写store逻辑
- 最后移除MobX依赖
14. 架构设计与模式
14.1 分层架构
- UI层:只负责展示和用户交互
- 状态管理层:处理业务逻辑和状态更新
- 服务层:封装API调用和数据处理
- 工具层:提供通用工具和中间件
14.2 领域驱动设计
- 按业务领域组织slice:
code复制/src
/features
/product
/order
/user
- 领域事件模式:
javascript复制const productSlice = createSlice({
reducers: {
productPurchased: (state, action) => {
// 处理购买逻辑
}
}
})
const userSlice = createSlice({
extraReducers: (builder) => {
builder.addCase(productPurchased, (state, action) => {
// 更新用户相关状态
})
}
})
14.3 CQRS模式实现
javascript复制// 命令端
const cartSlice = createSlice({
reducers: {
addItem: (state, action) => {
// 修改状态
}
}
})
// 查询端
const selectCartSummary = createSelector(
[selectCartItems],
(items) => ({
total: items.reduce((sum, item) => sum + item.price, 0),
count: items.length
})
)
15. 未来演进与替代方案
15.1 Redux未来路线图
- RTK Query成为核心:更深度集成数据获取能力
- 更好的TypeScript支持:减少手动类型定义
- 更小的包体积:优化tree-shaking
- 更智能的开发工具:增强调试体验
15.2 替代方案比较
-
Context API:
- 适合简单状态共享
- 缺乏中间件、时间旅行等能力
- 性能优化需要手动处理
-
Zustand:
- 更简单的API
- 不需要action/reducer
- 适合中小型应用
-
Jotai:
- 原子化状态管理
- 自动依赖跟踪
- 更适合细粒度更新
15.3 技术选型建议
-
选择Redux当:
- 需要强大的开发工具支持
- 有复杂的状态管理需求
- 需要中间件生态系统
- 团队已有Redux经验
-
考虑替代方案当:
- 应用规模较小
- 需要更简单的学习曲线
- 追求更小的包体积
- 需要更灵活的状态模型
16. 实战技巧与经验分享
16.1 调试技巧
- 状态快照比较:
javascript复制let prevState = store.getState()
store.subscribe(() => {
const currState = store.getState()
console.log('Diff:', deepDiff(prevState, currState))
prevState = currState
})
- Action追踪:
javascript复制const logActionsMiddleware = store => next => action => {
console.groupCollapsed(`ACTION ${action.type}`)
console.log('Payload:', action.payload)
const result = next(action)
console.log('Next state:', store.getState())
console.groupEnd()
return result
}
16.2 性能优化
- 批量更新:
javascript复制const batchActionsMiddleware = store => next => action => {
if(Array.isArray(action)) {
return action.map(a => next(a))
}
return next(action)
}
- 选择性订阅:
javascript复制function UserProfile({ userId }) {
const user = useSelector(state =>
state.users.entities[userId], shallowEqual
)
// ...
}
16.3 团队协作
-
代码规范:
- 统一的slice文件结构
- 命名约定(action命名、selector前缀等)
- 类型定义规范
-
文档实践:
- 为每个slice添加JSDoc
- 维护状态结构文档
- 记录常见action流程
-
评审重点:
- 状态序列化检查
- 选择器记忆化
- 异步错误处理
- 性能敏感操作
17. 状态设计模式
17.1 标准化状态
javascript复制{
books: {
ids: ['book1', 'book2'],
entities: {
'book1': { id: 'book1', title: 'Book 1' },
'book2': { id: 'book2', title: 'Book 2' }
}
}
}
17.2 有限状态机
javascript复制const orderSlice = createSlice({
name: 'order',
initialState: {
status: 'idle', // 'loading', 'success', 'error'
data: null,
error: null
},
reducers: {
startLoading: (state) => {
state.status = 'loading'
},
success: (state, action) => {
state.status = 'success'
state.data = action.payload
},
error: (state, action) => {
state.status = 'error'
state.error = action.payload
}
}
})
17.3 乐观更新
javascript复制const postsSlice = createSlice({
reducers: {
addPost: {
reducer: (state, action) => {
state.entities[action.payload.id] = action.payload
},
prepare: (post) => {
const id = nanoid()
return { payload: { ...post, id, isOptimistic: true } }
}
},
confirmAddPost: (state, action) => {
const post = state.entities[action.meta.id]
if(post) {
post.isOptimistic = false
post.id = action.payload.id // 服务器生成的真实ID
}
}
}
})
18. 测试策略进阶
18.1 集成测试
javascript复制test('should handle complete checkout flow', async () => {
const store = configureStore({
reducer: {
cart: cartReducer,
checkout: checkoutReducer
}
})
// 添加商品
store.dispatch(addItem({ id: 1, name: 'Product', price: 10 }))
// 开始结账
await store.dispatch(initiateCheckout())
// 验证状态
const state = store.getState()
expect(state.cart.items).toHaveLength(0)
expect(state.checkout.lastOrder).not.toBeNull()
})
18.2 E2E测试
javascript复制describe('Cart Checkout', () => {
it('should complete purchase', () => {
// 启动应用
cy.visit('/')
// 添加商品
cy.get('[data-testid="add-to-cart"]').click()
// 结账
cy.get('[data-testid="checkout"]').click()
// 验证订单完成
cy.contains('Order confirmed').should('be.visible')
})
})
18.3 性能测试
javascript复制describe('Redux performance', () => {
it('should handle 1000 actions under 1s', () => {
const store = configureStore({ reducer })
const start = performance.now()
for(let i = 0; i < 1000; i++) {
store.dispatch({ type: 'TEST_ACTION', payload: i })
}
const duration = performance.now() - start
expect(duration).toBeLessThan(1000)
})
})
19. 安全最佳实践
19.1 状态安全
-
敏感数据保护:
- 避免在Redux存储敏感信息
- 使用加密中间件处理敏感字段
- 实现自动清理机制
-
状态验证:
javascript复制const validationMiddleware = store => next => action => {
const result = next(action)
validateState(store.getState())
return result
}
19.2 操作审计
javascript复制const auditMiddleware = store => next => action => {
const user = store.getState().auth.user
logAction({
type: action.type,
user: user?.id,
timestamp: Date.now()
})
return next(action)
}
19.3 防篡改保护
javascript复制const tamperProofMiddleware = store => next => action => {
if(action.__signature !== expectedSignature(action)) {
throw new Error('Invalid action signature')
}
return next(action)
}
20. 总结与资源推荐
20.1 学习路径建议
-
入门阶段:
- 掌握Redux核心概念
- 熟悉RTK基础API
- 实现简单CRUD操作
-
进阶阶段:
- 深入理解中间件
- 掌握性能优化技巧
- 学习状态设计模式
-
专家阶段:
- 定制中间件开发
- 复杂状态架构设计
- 性能分析与调优
20.2 推荐资源
-
官方文档:
-
社区资源:
- Redux GitHub仓库
- Reactiflux Discord社区
- Redux相关博客文章
-
工具链:
- Redux DevTools扩展
- Immer for immutable updates
- Reselect for memoized selectors
20.3 持续学习建议
- 跟进更新:定期查看Redux Toolkit的发布说明
- 实践项目:通过实际项目应用所学知识
- 参与社区:贡献代码、回答问题、分享经验
- 探索源码:深入理解内部实现原理
