从Dva到Redux ToolKit,现代Redux的演进
前言
2015年发布的Redux至今仍是React生态中最常用的状态管理库,2022年4月19日发布的Redux [v4.2.0](https://github.com/reduxjs/redux/releases/tag/v4.2.0)
中正式将createStore
方法标记为“已弃用”(@deprecated
),并推荐用户使用Redux-Toolkit(下面简称为RTK)的configureStore
方法。
此次发布只是增加了额外的
@deprecate
提示,并不影响任何存量的代码,也不会有运行时的错误提示
可能在此之前很多Redux用户并没有了解过RTK(2021年Redux的占有率在45 - 50%,而同时用到RTK的仅有4.5%),而是用了其他基于Redux的封装来简化Redux的配置和使用,比如曾经一度流行的Redux封装Dva,现在周下载量仍有2w6+,还有相当多的存量应用。不过Dva上一次的正式版发布已经是三年多前了,事实上处于不维护状态。
这期间2019年React 16.8推出了Hook,整个React生态尤其是状态管理库也随之开始转向,大量库都设计了易用性更高的Hook API。因为啰嗦的模板代码写法被饱受诟病的Redux在React-Redux v7.1也推出了Hook API
useSelector/useDispatch
,随后正式推出了社区期盼已久的开箱即用Redux解决方案:Redux ToolKit(前身是Redux-Starter-Kit)。
为什么现在Redux要强推RTK和其代表的“现代Redux”?借此次发布的契机,来聊一聊现代Redux的一些演进
经典Redux
Redux是什么?
Redux是Flux架构的一种扩展或者实现,具有同样的单向数据流,最初是2015年Dan Abramov想要在Flux应用上新增热替换和时间旅行功能而开发的,为了让状态修改“可预测”。
下面来看一看Redux核心
createStore
的一个最小实现:
function createStore(reducer) {
let state;
let listeners = [];
let currentReducer = reducer;
function getState() {
return state;
}
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
const idx = listeners.indexOf(listener);
listeners.splice(idx, 1);
};
}
function dispatch(action) {
state = currentReducer(state, action);
listeners.forEach((listener) => listener());
}
function replaceReducer(nextReducer) {
currentReducer = nextReducer;
dispatch({ type: "replace" });
return store;
}
// 初始化各个reducer的状态树
dispatch({ type: "init" });
const store = { getState, subscribe, dispatch, replaceReducer };
return store;
}
- 除去进阶的enhancers等功能,Redux核心就是一个简单的发布订阅模式,当action被dispatch时通知各个listener,只是限制了action必须是普通的对象。
- 为了实现热替换,Redux将Flux Store中的状态和状态更新逻辑(Reducer)分离,更新Reducer时只需要
replaceReducer
,而不会丢失当前状态。
Redux有著名的三个原则
- 单一数据源:全局状态都存放在一个单个Store的对象树。
- 只读的State,需要通过触发一个Action对象来修改,Action描述了要发生的修改。
- 使用纯函数修改State,reducer纯函数每次都会返回新的状态对象。
但这些原则并不是强制性的,比如大的应用可能拆分了多个Store、state可能在其他地方被直接修改、reducers触发了副作用等等。Redux实现本身并没有在Store或者Reducers上做任何检查或者限制,Redux Core被设计成最小API以及高度可扩展。三原则只是描述了Redux范式应该是怎么样的,具体实现和约束完全交给用户实现,因此Redux的生态非常繁荣,各种中间价百花齐放。
- 为了实现可预测的状态修改和时间旅行,引入了immutability和serializability ,这样每次状态对象变化不直接修改状态,而是生成新的对象,因此能够保存旧的状态,通过Redux DevTool就能追踪每一次的Action的带来的修改。
- 因为所有的状态都放在一个Store里,为了方便维护,可以将大的Reducer拆分多个子Reducer,每个子Reducer只管理对应的状态切片(State Slice),最后通过
combineReducers()
组合到一起。 - 因为Reducer都应该是纯函数进行同步操作,为了实现副作用和异步逻辑,社区上出现了如redux-thunk (async/await写法),redux-saga (generator写法,dva内置)等中间件实现。
- 为了方便维护和阅读,Action对象的Type一般是String,并引入了Action Creator(返回Action的函数)
- ...
Redux的中间件其实就是改写了Redux Store里的dispatch方法,官方文档:
const logger = (store) => (next) => (action) => {
console.log("dispatching", action);
let result = next(action);
console.log("next state", store.getState());
return result;
};
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
let dispatch = store.dispatch
middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
return { ...store, dispatch }
}
React-Redux
以上只是Redux核心库,要用于React还需要引入React-Redux库做UI Binding,通过高阶组件connect
和mapState/mapDispatch
来订阅Redux Store并通知React更新UI。具体来说就是将组件需要的state和dispath注入到props
里,因为性能优化和React API变更等原因,React-Redux的迭代远比Redux核心库频繁。
- v4及之前
connect
在componentDidMount
里订阅Store,每一个父组件和其子组件的connect
都会独立订阅。并且每次对Store的修改总会触发connect
re-render,mapState
等逻辑都是放在render阶段。 - v5.x重写了
connect
的逻辑为自顶向下订阅,因为componentDidMount
的执行顺序是从子组件到父组件,因此之前版本里子组件先订阅到了Redux Store,可能导致和父组件传递的Props不一致的问题。同时将状态派生等逻辑从React render中移除,只有mapState
结果不一样才会触发re-render,因此v5的性能比之前版本提升很多。 - v6发布在React 16.3推出新的
createContext()
API 取代legacy context之后,为了和未来的“Concurrent React”兼容,直接将原本在Store实例内部的State放在了createContext()
的context里,只有Provider
组件真正订阅了Store,其他子组件都依赖context本身来触发re-render(自顶向下)。因为React context需要遍历整个组件树来找到consumer来触发re-render,导致v6在几乎所有场景都比v5慢,越复杂的场景越慢。
Redux多年发展下来积攒了大量的“最佳实践”,几乎很少会只使用Redux Core进行开发,为了实现各种特性都有大量的中间件可以选择。开发一个Redux应用需要的配置和模版代码也就越来越多,比如大家熟悉的经典Redux全家桶:Webpack + React + React-Redux + Redux + Redux DevTools + Saga/Thunk + reselect + actionCreator+normalizr,reducer纯函数的写法在修改深对象和数组时也需要许多额外逻辑。
现代Redux
因为Redux的推广非常成功,以至于几乎和React绑定到一起,大量的用户从接触React起就开始使用Redux,配置和中间件等都照搬的模板,实际上Redux本身的灵活性反而成了普通用户的累赘,使得上手成本非常高。这个阶段许多像Dva的框架都开始尝试提供开箱即用的Redux,只暴露出少量的API 。 同时,
-
TypeScript开始兴起,给Redux Reducer/Store/Action/connect添加类型定义变得麻烦起来,几乎都需要用户自己手写。
-
React 16.8发布了Hook API,提供了
useContext
和useReudcer
来实现类Redux的状态管理
Redux也积极响应变化,先是React-Redux v7使用Hook来重写connect
,并在v7.1提供了useSelector/useDispatch
Hook,Redux Starter Kit开始用TypeScript重写,然后改写了所有文档和实践,推荐用户直接使用Hook API而不是connect
。直到现在,Redux和RTK中会经常提到“现代”这个词,可以简单概况为以下实践:
- 不需要手写冗长的Redux模板和配置代码(比如ActionCreator)
- React-Redux Hook API取代麻烦的
connect
和mapState
- Reducer中使用Immer来更新Immutable数据
- 更简单的TypeScript集成
- 内置安全检查
- Immutability
- Serializability
- 按feature组织Redux逻辑
- 抽象异步数据的获取、变更和缓存逻辑
- 包含了必要的中间件
把以上这些最佳实践组合到一起就是如今的RTK,仍然是经典的Redux,仍然有灵活性,但易用且低门槛。
快速上手
安装
因为RTK只是处理Redux的核心逻辑,Store和React之间的通讯还需要React-Redux(useSelecotr, useDispatch
)
gzip大小
- @reduxjs/toolkit:12.7k
- react-redux:4.7k
- jotai: 3.7k
npm install @reduxjs/toolkit react-redux
创建并连接Store
通过configureStore
API配置一个Redux Store,简化了以往繁杂的组合reducer,middleware,devTool, enhancer等流程,默认配置基本上保证了开箱即用。
// store.ts
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
// Root Reducer或者RTK的Slice Reducer组成的Map
reducer: {
// TODO
},
// middleware: [],
// 启用Redux DevTools,默认true
// devTools: true,
})
其中
middleware
默认情况下在开发环境是[thunk, immutableStateInvariant, serializableStateInvariant]
,在生产环境仅保留thunk
。
通过React-Redux的Provider
组件包裹React App来传递Redux Store
// index.ts
import { createRoot } from 'react-dom/client'
import { store } from './store'
import { Provider } from 'react-redux'
import App from './App'
const root = createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
创建Redux Slice
通过createSlice
API来创建一个“状态切片”,即包含了namespace,initialState,reducers,action的集合体,也是官方推荐的标准Redux写法。
createSlice
封装了slice reducer,selector,immer,action creator等逻辑。
// slice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
export interface CounterState {
value: number
}
const initialState: CounterState = {
value: 0,
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
// 内置immer,可以直接更改状态
state.value += 1
},
incrementByAmount(state, action: PayloadAction<number>) {
state.value += action.payload
},
},
})
export const { increment, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
createSlice
的返回值
{
name : string,
reducer : ReducerFunction,
actions : Record<string, ActionCreator>,
caseReducers: Record<string, CaseReducer>.
getInitialState: () => State
}
然后将返回的reducer
添加到configureStore
中的reducer
字段里
// store.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './slice'
export const store = configureStore({
reducer: {
counter: counterReducer
},
作为对比,下面是Dva中的一个典型Model例子,两者在约定的写法上没有多少出入。
// 1. Initialize
const app = dva();
// 2. Model
const model = {
namespace: 'count',
state: 0,
reducers: {
add (count) { return count + 1 },
minus(count) { return count - 1 },
reset() { return 0 },
},
}
// 3. 绑定
app.model(model);
在React组件中使用State
- 通过
useSelector
来读取Store中的状态 - 通过
useDispatch
来派生状态,这里可以用createSlice
返回的action来简化。
// Counter.ts
import { useSelector, useDispatch } from 'react-redux'
import { increment } from './slice'
// counter就是添加Slice时用的key
const selectCount = (state) => state.counter.value
const Counter() {
const count = useSelector(selectCount)
const dispatch = useDispatch()
// 相当于dispatch({ type: 'counter/increment' })
const onInc = () => dispatch(increment())
return (
<div>
<button onClick={onInc}>+</button>
<span>{count}</span>
</div>
)
}
TypeScript支持
这里只简单介绍基础用法,实际情况下TypeScript类型定义会复杂很多,这也是Redux一直以来的痛点,详见官方文档
// store.ts
/// store = configureStore(....)
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
为了避免在每次使用useSelector
和useDispatch
都带上这两个类型,可以重新定义这两个Hook
// hook.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
副作用/异步逻辑
因为Redux Store本身只有同步Dispatch Action的能力,reducer也不应该包含任何副作用,副作用通常需要第三方中间件,如redux-thunk (async/await写法),redux-saga (generator写法,dva内置)。
在RTK中默认启用了redux-thunk作为异步逻辑中间件,Thunk在Redux中指返回值为函数的action 生成器。
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
usersLoading(state, action) {
state.loading = 'loading'
},
usersFetched(state, action) {
state.loading = 'idle'
state.entities = action.payload
}
},
})
const { usersLoading, usersFetched } = usersSlice.actions
const fetchUserById = (userId) => async (dispatch, getState) => {
dispatch(usersLoading())
const response = await userAPI.fetchById(userId)
dispatch(usersFetched(response.data))
}
同样的,RTK提供了createAsyncThunk
来简化Thunk Action的定义流程:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// 创建thunk action创建函数
// 包含本身定义的用户逻辑,pending, fulfilled, rejected 4种action
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {},
// 在extraReducer中可以定义reducer来响应Slice外部的action
// 比如这里的fetchUserById就是外部定义的Thunk Action
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {
state.loading = 'loading'
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.entities.push(action.payload)
})
},
})
// dispatch thunk, 相当于
// 1. dispatch pending
// 2. dispatch 用户逻辑
// 3. dispatch fulfilled 或 rejected
dispatch(fetchUserById(123))
简单实现createSlice
可以看出createSlice
是简化模板代码最多的reducer和action的关键API,其实核心实现也比较的简单:
包含了createAction
和createReducer
两个工具API
这里的简单实现忽略了immer的集成和Action Matcher相关逻辑
createAction
:返回一个action创建器
function createAction(type) {
function actionCreator(...args) {
return { type, payload: args[0] }
}
actionCreator.toString = () => `${type}`
actionCreator.type = type
actionCreator.match = (action) => action.type === type
return actionCreator
}
createReducer
:构建action type的映射Map
function createReducer(initialState, actionsMap) {
function reducer(state = initialState, action) {
const caseReducer = actionsMap[action.type];
if (caseReducer) {
return caseReducer(state, action)
}
return previousState
}
reducer.getInitialState = () => initialState;
return reducer
}
createSlice
function getType(slice, actionKey) {
return `${slice}/${actionKey}`
}
function createSlice(options) {
if (!options.name) {
throw new Error('`name` is a required option for createSlice')
}
const { name, initialState, reducers = {} } = options
const reducerNames = Object.keys(reducers)
const sliceCaseReducersByName = {}
const sliceCaseReducersByType = {}
const actionCreators = {}
reducerNames.forEach((reducerName) => {
const caseReducer = reducers[reducerName]
const type = getType(name, reducerName)
sliceCaseReducersByName[reducerName] = caseReducer
sliceCaseReducersByType[type] = caseReducer
actionCreators[reducerName] = createAction(type)
})
function buildReducer() {
// 这里还会有extraReducer的处理,这里省略
const finalCaseReducers = { ...sliceCaseReducersByType }
return createReducer(initialState, finalCaseReducers)
}
let _reducer;
return {
name,
reducer(state, action) {
if (!_reducer) _reducer = buildReducer()
return _reducer(state, action)
},
actions: actionCreators,
caseReducers: sliceCaseReducersByName,
getInitialState() {
if (!_reducer) _reducer = buildReducer()
return _reducer.getInitialState()
},
}
}
更进一步 - RTK Query
在实际的应用场景中,除了一般的客户端状态管理,还常常充斥着异步数据获取与缓存的复杂状态逻辑。尽管RTK中已经提供了createSlice
和createAsyncThunk
来简化异步数据的流程,但仍然有大量类似的逻辑需要开发者处理。
因此基于React社区经验(react-query/useSWR)和RTK本身,Redux官方推出了RTK Query来解决异步数据的问题。
虽然下面的例子都是React的,但和RTK一样,RTK Query的实现也是和具体UI框架无关
快速入门
社区其他实现习惯是将每个API分开定义,每一个都是单独的自定义hook,而在RTK Query需要通过createApi
定义一个API服务,所有的API endpoint都集中在一块,产物是一个API Slice(类似于Redux Slice)
endpoints按用途分为了两类:查询(query)和突变(mutation)
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({baseUrl: '/'}),
endpoints: (builder) => ({
// 定义查询
getPost: builder.query({
query: (id) => `post/${id}`,
}),
// 定义突变
addPost: builder.mutation({
query: (body) => ({
url: `posts`,
method: 'POST',
body,
}),
}),
})
})
// 自动生成了每个Endpoint对应的useQuery/useMutation Hook
export const { useGetPostsQuery, useAddPostMutation } = api
export const { endpoints, reducerPath, reducer, middleware } = api
// endpoints也包含hooks
api.endpoints.getPosts.useQuery // useGetPostsQuery
api.endpoints.updatePost.useMutation // useAddPostMutation
其中通过自定义baseQuery
就可以实现全局的请求拦截器。如果需要单独处理某个endPoint
的请求行为,可以通过onQueryStarted
实现,该函数会在请求的整个生命周期(发起/成功/失败)中被调用,或者定义queryFn
来绕过baseQuery
进行查询。
// 函数签名
async function onQueryStarted(
arg: QueryArg,
{
dispatch,
getState,
extra,
requestId,
queryFulfilled, // Promise
getCacheEntry,
updateCachedData,
}: QueryLifecycleApi
): Promise<void>
// 例子
const api = createApi({
baseQuery: fetchBaseQuery({baseUrl: '/'}),
endpoints: (build) => ({
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
async onQueryStarted(id, { dispatch, queryFulfilled }) {
// 请求开始
dispatch('...')
try {
const { data } = await queryFulfilled
// 请求成功
dispatch('...')
} catch (err) {
// 请求失败
dispatch('...')
}
},
}),
}),
})
API Slice
主要包含了以下部分:
- Redux Reducer:管理缓存数据的reducer
- Redux MiddleWare:管理缓存数据的生命周期和订阅的中间件
- Endpoints:根据用户定义的endpoints生成的Redux相关逻辑,同时也包含Hooks
- Utils: 提供一系列的Action Creator用于手动管理缓存数据
- Hooks:根据endpoints生成的数据获取React Hook
type Api = {
// Redux 集成
reducerPath: string;
reducer: Reducer;
middleware: Middleware;
// Endpoint 交互
endpoints: Record<string, EndpointDefinition>;
util: {
// ...
updateQueryData: UpdateQueryDataThunk;
patchQueryData: PatchQueryDataThunk;
prefetch: PrefetchThunk;
invalidateTags: ActionCreatorWithPayload<
Array<TagTypes | FullTagDescription<TagTypes>>,
string
>;
resetApiState: SliceActions["resetApiState"];
selectInvalidatedBy: (
state: RootState<Definitions, string, ReducerPath>,
tags: ReadonlyArray<TagDescription<TagTypes>>
) => Array<{
endpointName: string;
originalArgs: any;
queryCacheKey: string;
}>;
// ...
};
// 自动生成的 React hooks
[key in GeneratedReactHooks]: GeneratedReactHooks[key];
};
查询 & 突变
useQuery
用于获取服务端数据,和useSWR
,react-query
,ahooks的useRequest
等请求Hook的用法类似。
主要包含以下特性:
- 轮询
- 缓存
- 依赖刷新
- 聚焦重新请求
- 条件查询(ready/skip)
const {
data,
error,
isLoading,
isError,
isFetching,
refetch // 强制重新请求的函数
} = useGetPostQuery(id, {
skip: false,
pollingInterval: 10_000,
refetchOnFocus: true,
selectFromResult: undefined, // 根据选择器,只订阅结果的一部分,其他部分改变不会引起重渲染
refetchOnReconnect: true,
refetchOnMountOrArgChange: true
});
因为本质上还是订阅了Redux Store,所以useQuery
的selectFromResult
同样遵循selector
的原则,如果返回的引用发生改变(比如map/filter
等操作)就会使优化失效。
如果需要组件级别来派生数据并正确缓存,则需要每一个组件有一个唯一的reselect
选择器实例,可以通过组合useMemo
和reselect
来实现:
import { createSelector } from '@reduxjs/toolkit'
import { useGetPostsQuery } from '../api/apiSlice'
// useMemo保证该选择器在当前组件的渲染期间引用不变
const selectPostsForUser = useMemo(() => {
const emptyArray = [];
// 返回记忆化的选择器实例
return createSelector(
[(res) => res.data, (res, name) => name],
(data, name) =>
data?.filter((post) => post.name.includes(name)) ?? emptyArray
);
}, []);
const { filteredPosts } = useGetPostsQuery(undefined, {
selectFromResult: (result) => ({
...result,
filteredPosts: selectPostsForName(result, name)
})
})
useMutation
用于发送数据到服务端并更新本地缓存
const [updatePost, result] = useAddPostMutation();
const { isLoading, data } = result;
updatePost({
// ...
})
缓存
缓存是这类异步请求库的核心特性,RTK Query将每个查询的endpoint和参数序列化为字符串当作queryCacheKey
,queryCacheKey
相同的查询会共享同一个请求和缓存。当查询的引用计数为0(即没有组件用到),在一定时间后缓存将会被自动清除。除了根据queryCacheKey
进行缓存,RTK Query还可以通过缓存标签中间件实现自动的缓存管理。
比如以下简单的CURD配置,给Post相关的查询标记为'Posts'
,而Post相关的突变使有'Posts'
标签的缓存失效。因此当发生增删改后,对应Post的查询缓存就会失效并自动重新发起请求,标签还可以指定id
来匹配特定查询。
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
interface Tag {
type: string;
id?: string | number;
}
export const postApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query({
query: () => 'posts',
providesTags: [{ type: 'Posts', id: 'LIST' }],
}),
getPost: build.query({
query: (id) => `post/${id}`,
providesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
addPost: build.mutation({
query(body) {
return {
url: `post`,
method: 'POST',
body,
}
},
invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
}),
deletePost: build.mutation({
query(id) {
return {
url: `post/${id}`,
method: 'DELETE',
}
},
invalidatesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
}),
})
标签中间件的部分实现:
// packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts
function invalidateTags(
tags: readonly FullTagDescription<string>[],
mwApi: SubMiddlewareApi
) {
const rootState = mwApi.getState()
const state = rootState[reducerPath]
const toInvalidate = api.util.selectInvalidatedBy(rootState, tags)
context.batch(() => {
const valuesArray = Array.from(toInvalidate.values())
for (const { queryCacheKey } of valuesArray) {
const querySubState = state.queries[queryCacheKey]
const subscriptionSubState = state.subscriptions[queryCacheKey]
if (querySubState && subscriptionSubState) {
if (Object.keys(subscriptionSubState).length === 0) {
// 引用计数为0时直接清理缓存
mwApi.dispatch(
removeQueryResult({
queryCacheKey: queryCacheKey as QueryCacheKey,
})
)
} else if (querySubState.status !== QueryStatus.uninitialized) {
// 存在引用且已经查询过,重新查询并更新缓存
mwApi.dispatch(refetchQuery(querySubState, queryCacheKey))
} else {
}
}
}
})
}
总结
虽然近年来React社区的状态管理方案层出不穷,比如原子式的jotai
和recoil
,基于proxy的Valtio
,但Redux仍然是使用最广泛,影响力最大的,生态最繁荣的一个方案。官方这次力推的RTK和RTK Query通过大量抽象和简化已经基本上做到和其他轻量库类似的开发体验(包括TypeScript类型补全),同时兼具效率和可扩展性(虽然API数量还是不少)。如果已经在用Redux类的状态管理,不妨往前迈一步,尝试下现代Redux开发。
转载自:https://juejin.cn/post/7114120958637506591