Skip to main content

迁移到现代 Redux

你将学到什么
  • 如何对旧版 "hand-written" Redux 逻辑进行现代化改造以使用 Redux Toolkit

    ¥How to modernize legacy "hand-written" Redux logic to use Redux Toolkit

  • 如何对旧版 React-Redux connect 组件进行现代化改造以使用 hooks API

    ¥How to modernize legacy React-Redux connect components to use the hooks API

  • 如何对使用 TypeScript 的 Redux 逻辑和 React-Redux 组件进行现代化改造

    ¥How to modernize Redux logic and React-Redux components that use TypeScript

概述

¥Overview

Redux 于 2015 年问世,多年来我们推荐的 Redux 代码编写模式发生了显着变化。就像 React 从 createClassReact.Component 发展到带有 hooks 的功能组件一样,Redux 也从手动存储设置 + 手写带有对象扩展的 reducer + React-Redux 的 connect,发展到 Redux Toolkit 的 configureStore + createSlice + React-Redux 的 hooks API。

¥Redux has been around since 2015, and our recommended patterns for writing Redux code have changed significantly over the years. In the same way that React has evolved from createClass to React.Component to function components with hooks, Redux has evolved from manual store setup + hand-written reducers with object spreads + React-Redux's connect, to Redux Toolkit's configureStore + createSlice + React-Redux's hooks API.

许多用户正在使用较旧的 Redux 代码库,这些代码库在这些 "现代回归" 模式存在之前就已经存在。将这些代码库迁移到今天推荐的现代 Redux 模式将导致代码库更小且更易于维护。

¥Many users are working on older Redux codebases that have been around since before these "modern Redux" patterns existed. Migrating those codebases to today's recommended modern Redux patterns will result in codebases that are much smaller and easier to maintain.

好消息是,你可以逐步将代码迁移到现代 Redux,让新旧 Redux 代码共存并协同工作!

¥The good news is that you can migrate your code to modern Redux incrementally, piece by piece, with old and new Redux code coexisting and working together!

本页介绍了可用于对现有旧版 Redux 代码库进行现代化改造的一般方法和技术。

¥This page covers the general approaches and techniques you can use to modernize an existing legacy Redux codebase.

信息

有关 "现代回归" 与 Redux Toolkit + React-Redux hooks 如何简化 Redux 使用的更多详细信息,请参阅以下附加资源:

¥For more details on how "modern Redux" with Redux Toolkit + React-Redux hooks simplifies using Redux, see these additional resources:

使用 Redux Toolkit 实现 Redux 逻辑现代化

¥Modernizing Redux Logic with Redux Toolkit

迁移 Redux 逻辑的一般方法是:

¥The general approach to migrating Redux logic is:

  • 用 Redux Toolkit 的 configureStore 替换现有的手动 Redux 存储设置

    ¥Replace the existing manual Redux store setup with Redux Toolkit's configureStore

  • 选择一个现有的切片 reducer 及其关联的操作。将其替换为 RTK 的 createSlice。一次对一个 reducer 重复此操作。

    ¥Pick an existing slice reducer and its associated actions. Replace those with RTK's createSlice. Repeat for one reducer at a time.

  • 根据需要,用 RTK Query 或 createAsyncThunk 替换现有的数据获取逻辑

    ¥As needed, replace existing data fetching logic with RTK Query or createAsyncThunk

  • 根据需要使用 RTK 的其他 API,如 createListenerMiddlewarecreateEntityAdapter

    ¥Use RTK's other APIs like createListenerMiddleware or createEntityAdapter as needed

你应该始终从用 configureStore 替换旧的 createStore 调用开始。这是一次性步骤,所有现有的 reducer 和中间件将继续按原样工作。configureStore 包括对常见错误(如意外突变和不可序列化值)的开发模式检查,因此将这些检查到位将有助于识别代码库中发生这些错误的任何区域。

¥You should always start by replacing the legacy createStore call with configureStore. This is a one-time step, and all of the existing reducers and middleware will continue to work as-is. configureStore includes development-mode checks for common mistakes like accidental mutations and non-serializable values, so having those in place will help identify any areas of the codebase where those mistakes are happening.

信息

你可以在 Redux 基础知识,第 8 部分:使用 Redux 工具包的现代 Redux 中看到这种一般方法的实际应用。

¥You can see this general approach in action in Redux Fundamentals, Part 8: Modern Redux with Redux Toolkit.

使用 configureStore 存储设置

¥Store Setup with configureStore

典型的旧 Redux 存储设置文件执行几个不同的步骤:

¥A typical legacy Redux store setup file does several different steps:

  • 将切片 reducer 组合成根 reducer

    ¥Combining the slice reducers into the root reducer

  • 创建中间件增强器,通常使用 thunk 中间件,也可能使用开发模式下的其他中间件,例如 redux-logger

    ¥Creating the middleware enhancer, usually with the thunk middleware, and possibly other middleware in development mode such as redux-logger

  • 添加 Redux DevTools 增强器,并将增强器组合在一起

    ¥Adding the Redux DevTools enhancer, and composing the enhancers together

  • 调用 createStore

    ¥Calling createStore

这些步骤在现有应用中可能如下所示:

¥Here's what those steps might look like in an existing application:

src/app/store.js
import { createStore, applyMiddleware, combineReducers, compose } from 'redux'
import thunk from 'redux-thunk'

import postsReducer from '../reducers/postsReducer'
import usersReducer from '../reducers/usersReducer'

const rootReducer = combineReducers({
posts: postsReducer,
users: usersReducer
})

const middlewareEnhancer = applyMiddleware(thunk)

const composeWithDevTools =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

const composedEnhancers = composeWithDevTools(middlewareEnhancer)

const store = createStore(rootReducer, composedEnhancers)

所有这些步骤都可以通过对 Redux Toolkit 的 configureStore API 的一次调用来替换。

¥All of those steps can be replaced with a single call to Redux Toolkit's configureStore API.

RTK 的 configureStore 封装了原始 createStore 方法,并自动为我们处理大部分存储设置。事实上,我们可以有效地将其简化为一步:

¥RTK's configureStore wraps around the original createStore method, and handles most of the store setup for us automatically. In fact, we can cut it down to effectively one step:

Basic Store Setup: src/app/store.js
import { configureStore } from '@reduxjs/toolkit'

import postsReducer from '../reducers/postsReducer'
import usersReducer from '../reducers/usersReducer'

// Automatically adds the thunk middleware and the Redux DevTools extension
const store = configureStore({
// Automatically calls `combineReducers`
reducer: {
posts: postsReducer,
users: usersReducer
}
})

configureStore 的一次调用为我们完成了所有工作:

¥That one call to configureStore did all the work for us:

  • 它调用 combineReducerspostsReducerusersReducer 组合到根 reducer 函数中,该函数将处理看起来像 {posts, users} 的根状态

    ¥It called combineReducers to combine postsReducer and usersReducer into the root reducer function, which will handle a root state that looks like {posts, users}

  • 它调用 createStore 使用该根 reducer 创建 Redux 存储

    ¥It called createStore to create a Redux store using that root reducer

  • 它自动添加了 thunk 中间件并调用了 applyMiddleware

    ¥It automatically added the thunk middleware and called applyMiddleware

  • 它会自动添加更多中间件来检查常见错误,例如意外改变状态

    ¥It automatically added more middleware to check for common mistakes like accidentally mutating the state

  • 它会自动设置 Redux DevTools Extension 连接

    ¥It automatically set up the Redux DevTools Extension connection

如果你的存储设置需要额外的步骤,例如添加额外的中间件、向 thunk 中间件传递 extra 参数或创建持久化根 reducer,你也可以这样做。这是一个更大的示例,展示了自定义内置中间件并打开 Redux-Persist,它演示了使用 configureStore 的一些选项:

¥If your store setup requires additional steps, such as adding additional middleware, passing in an extra argument to the thunk middleware, or creating a persisted root reducer, you can do that as well. Here's a larger example that shows customizing the built-in middleware and turning on Redux-Persist, which demonstrates some of the options for working with configureStore:

Detailed Example: Custom Store Setup with Persistence and Middleware

此示例显示了设置 Redux 存储时几个可能的常见任务:

¥This example shows several possible common tasks when setting up a Redux store:

  • 单独组合 reducer(有时由于其他架构限制而需要)

    ¥Combining the reducers separately (sometimes needed due to other architectural constraints)

  • 有条件和无条件添加额外的中间件

    ¥Adding additional middleware, both conditionally and unconditionally

  • 将 "额外的参数" 传递到 thunk 中间件,例如 API 服务层

    ¥Passing an "extra argument" into the thunk middleware, such as an API service layer

  • 使用 Redux-Persist 库,需要对其不可序列化的操作类型进行特殊处理

    ¥Using the Redux-Persist library, which requires special handling for its non-serializable action types

  • 在产品中关闭开发工具,并在开发中设置其他开发工具选项

    ¥Turning the devtools off in prod, and setting additional devtools options in development

这些都不是必需的,但它们确实经常出现在现实世界的代码库中。

¥None of these are required, but they do show up frequently in real-world codebases.

Custom Store Setup: src/app/store.js
import { configureStore, combineReducers } from '@reduxjs/toolkit'
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { PersistGate } from 'redux-persist/integration/react'
import logger from 'redux-logger'

import postsReducer from '../features/posts/postsSlice'
import usersReducer from '../features/users/usersSlice'
import { api } from '../features/api/apiSlice'
import { serviceLayer } from '../features/api/serviceLayer'

import stateSanitizerForDevtools from './devtools'
import customMiddleware from './someCustomMiddleware'

// Can call `combineReducers` yourself if needed
const rootReducer = combineReducers({
posts: postsReducer,
users: usersReducer,
[api.reducerPath]: api.reducer
})

const persistConfig = {
key: 'root',
version: 1,
storage
}

const persistedReducer = persistReducer(persistConfig, rootReducer)

const store = configureStore({
// Pass previously created persisted reducer
reducer: persistedReducer,
middleware: getDefaultMiddleware => {
const middleware = getDefaultMiddleware({
// Pass in a custom `extra` argument to the thunk middleware
thunk: {
extraArgument: { serviceLayer }
},
// Customize the built-in serializability dev check
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
}
}).concat(customMiddleware, api.middleware)

// Conditionally add another middleware in dev
if (process.env.NODE_ENV !== 'production') {
middleware.push(logger)
}

return middleware
},
// Turn off devtools in prod, or pass options in dev
devTools:
process.env.NODE_ENV === 'production'
? false
: {
stateSanitizer: stateSanitizerForDevtools
}
})

createSlice 的 reducer 和动作

¥Reducers and Actions with createSlice

典型的旧版 Redux 代码库的 reducer 逻辑、操作创建者和操作类型分布在不同的文件中,并且这些文件通常按类型位于不同的文件夹中。reducer 逻辑是使用 switch 语句和带有对象扩展和数组映射的手写不可变更新逻辑编写的:

¥A typical legacy Redux codebase has its reducer logic, action creators, and action types spread across separate files, and those files are often in separate folders by type. The reducer logic is written using switch statements and hand-written immutable update logic with object spreads and array mapping:

src/constants/todos.js
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
src/actions/todos.js
import { ADD_TODO, TOGGLE_TODO } from '../constants/todos'

export const addTodo = (id, text) => ({
type: ADD_TODO,
text,
id
})

export const toggleTodo = id => ({
type: TOGGLE_TODO,
id
})
src/reducers/todos.js
import { ADD_TODO, TOGGLE_TODO } from '../constants/todos'

const initialState = []

export default function todosReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO: {
return state.concat({
id: action.id,
text: action.text,
completed: false
})
}
case TOGGLE_TODO: {
return state.map(todo => {
if (todo.id !== action.id) {
return todo
}

return {
...todo,
completed: !todo.completed
}
})
}
default:
return state
}
}

Redux Toolkit 的 createSlice API 旨在通过编写 reducer、操作和不可变更新来消除所有 "boilerplate"!

¥Redux Toolkit's createSlice API was designed to eliminate all the "boilerplate" with writing reducers, actions, and immutable updates!

使用 Redux Toolkit,对旧版代码进行了多项更改:

¥With Redux Toolkit, there's multiple changes to that legacy code:

  • createSlice 将完全消除手写动作创建者和动作类型

    ¥createSlice will eliminate the hand-written action creators and action types entirely

  • 所有唯一命名的字段(例如 action.textaction.id)都被 action.payload 替换,无论是作为单个值还是包含这些字段的对象

    ¥All of the uniquely-named fields like action.text and action.id get replaced by action.payload, either as an individual value or an object containing those fields

  • 由于 Immer,手写的不可变更新被 reducer 中的 "mutating" 逻辑取代

    ¥The hand-written immutable updates are replaced by "mutating" logic in reducers thanks to Immer

  • 无需为每种类型的代码创建单独的文件

    ¥There's no need for separate files for each type of code

  • 我们教授将给定 reducer 的所有逻辑放在单个 "slice" 文件中

    ¥We teach having all logic for a given reducer in a single "slice" file

  • 我们建议按 "features" 组织文件,并将相关代码放在同一文件夹中,而不是按 "代码类型" 划分单独的文件夹

    ¥Instead of having separate folders by "type of code", we recommend organizing files by "features", with related code living in the same folder

  • 理想情况下,reducer 和 actions 的命名应该使用过去时并描述 "发生的一件事",而不是命令式的 "现在就做这件事",例如 todoAdded 而不是 ADD_TODO

    ¥Ideally, the naming of the reducers and actions should use the past tense and describe "a thing that happened", rather than an imperative "do this thing now", such as todoAdded instead of ADD_TODO

这些用于常量、操作和化简器的单独文件将全部被单个 "slice" 文件替换。现代化的切片文件如下所示:

¥Those separate files for constants, actions, and reducers, would all be replaced by a single "slice" file. The modernized slice file would look like this:

src/features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = []

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// Give case reducers meaningful past-tense "event"-style names
todoAdded(state, action) {
const { id, text } = action.payload
// "Mutating" update syntax thanks to Immer, and no `return` needed
state.todos.push({
id,
text,
completed: false
})
},
todoToggled(state, action) {
// Look for the specific nested object to update.
// In this case, `action.payload` is the default field in the action,
// and can hold the `id` value - no need for `action.id` separately
const matchingTodo = state.todos.find(todo => todo.id === action.payload)

if (matchingTodo) {
// Can directly "mutate" the nested object
matchingTodo.completed = !matchingTodo.completed
}
}
}
})

// `createSlice` automatically generated action creators with these names.
// export them as named exports from this "slice" file
export const { todoAdded, todoToggled } = todosSlice.actions

// Export the slice reducer as the default export
export default todosSlice.reducer

当你调用 dispatch(todoAdded('Buy milk')) 时,你传递给 todoAdded 操作创建者的任何单个值都将自动用作 action.payload 字段。如果需要传递多个值,请以对象的形式传递,例如 dispatch(todoAdded({id, text}))。或者,你可以使用 createSlice reducer 内部的 "prepare" 表示法 接受多个单独的参数并创建 payload 字段。prepare 表示法对于操作创建者执行额外工作(例如为每个项目生成唯一 ID)的情况也很有用。

¥When you call dispatch(todoAdded('Buy milk')), whatever single value you pass to the todoAdded action creator will automatically get used as the action.payload field. If you need to pass in multiple values, do so as an object, like dispatch(todoAdded({id, text})). Alternately, you can use the "prepare" notation inside of a createSlice reducer to accept multiple separate arguments and create the payload field. The prepare notation is also useful for cases where the action creators were doing additional work, such as generating unique IDs for each item.

虽然 Redux Toolkit 并不特别关心你的文件夹和文件结构或操作命名,但 这些是我们推荐的最佳实践 因为我们发现它们会带来更易于维护和理解的代码。

¥While Redux Toolkit does not specifically care about your folder and file structures or action naming, these are the best practices we recommend because we've found they lead to more maintainable and understandable code.

使用 RTK 查询获取数据

¥Data Fetching with RTK Query

React+Redux 应用中典型的旧版数据获取需要许多移动片段和类型的代码:

¥Typical legacy data fetching in a React+Redux app requires many moving pieces and types of code:

  • 代表 "请求开始"、"请求成功" 和 "请求失败" 操作的操作创建者和操作类型

    ¥Action creators and action types that represent "request starting", "request succeeded", and "request failed" actions

  • Thunks 分派操作并发出异步请求

    ¥Thunks to dispatch the actions and make the async request

  • 跟踪加载状态并存储缓存数据的 reducer

    ¥Reducers that track loading status and store the cached data

  • 用于从存储中读取这些值的选择器

    ¥Selectors to read those values from the store

  • 安装后通过类组件中的 componentDidMount 或功能组件中的 useEffect 调度组件中的 thunk

    ¥Dispatching the thunk in a component after mounting, either via componentDidMount in a class component or useEffect in a function component

这些通常会分为许多不同的文件:

¥These typically would be split across many different files:

src/constants/todos.js
export const FETCH_TODOS_STARTED = 'FETCH_TODOS_STARTED'
export const FETCH_TODOS_SUCCEEDED = 'FETCH_TODOS_SUCCEEDED'
export const FETCH_TODOS_FAILED = 'FETCH_TODOS_FAILED'
src/actions/todos.js
import axios from 'axios'
import {
FETCH_TODOS_STARTED,
FETCH_TODOS_SUCCEEDED,
FETCH_TODOS_FAILED
} from '../constants/todos'

export const fetchTodosStarted = () => ({
type: FETCH_TODOS_STARTED
})

export const fetchTodosSucceeded = todos => ({
type: FETCH_TODOS_SUCCEEDED,
todos
})

export const fetchTodosFailed = error => ({
type: FETCH_TODOS_FAILED,
error
})

export const fetchTodos = () => {
return async dispatch => {
dispatch(fetchTodosStarted())

try {
// Axios is common, but also `fetch`, or your own "API service" layer
const res = await axios.get('/todos')
dispatch(fetchTodosSucceeded(res.data))
} catch (err) {
dispatch(fetchTodosFailed(err))
}
}
}
src/reducers/todos.js
import {
FETCH_TODOS_STARTED,
FETCH_TODOS_SUCCEEDED,
FETCH_TODOS_FAILED
} from '../constants/todos'

const initialState = {
status: 'uninitialized',
todos: [],
error: null
}

export default function todosReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TODOS_STARTED: {
return {
...state,
status: 'loading'
}
}
case FETCH_TODOS_SUCCEEDED: {
return {
...state,
status: 'succeeded',
todos: action.todos
}
}
case FETCH_TODOS_FAILED: {
return {
...state,
status: 'failed',
todos: [],
error: action.error
}
}
default:
return state
}
}
src/selectors/todos.js
export const selectTodosStatus = state => state.todos.status
export const selectTodos = state => state.todos.todos
src/components/TodosList.js
import { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { fetchTodos } from '../actions/todos'
import { selectTodosStatus, selectTodos } from '../selectors/todos'

export function TodosList() {
const dispatch = useDispatch()
const status = useSelector(selectTodosStatus)
const todos = useSelector(selectTodos)

useEffect(() => {
dispatch(fetchTodos())
}, [dispatch])

// omit rendering logic here
}

许多用户可能正在使用 redux-saga 库来管理数据获取,在这种情况下,他们可能有额外的 "signal" 操作类型用于触发 sagas,并且此 saga 文件而不是 thunk:

¥Many users may be using the redux-saga library to manage data fetching, in which case they might have additional "signal" action types used to trigger the sagas, and this saga file instead of thunks:

src/sagas/todos.js
import { put, takeEvery, call } from 'redux-saga/effects'
import {
FETCH_TODOS_BEGIN,
fetchTodosStarted,
fetchTodosSucceeded,
fetchTodosFailed
} from '../actions/todos'

// Saga to actually fetch data
export function* fetchTodos() {
yield put(fetchTodosStarted())

try {
const res = yield call(axios.get, '/todos')
yield put(fetchTodosSucceeded(res.data))
} catch (err) {
yield put(fetchTodosFailed(err))
}
}

// "Watcher" saga that waits for a "signal" action, which is
// dispatched only to kick off logic, not to update state
export function* fetchTodosSaga() {
yield takeEvery(FETCH_TODOS_BEGIN, fetchTodos)
}

所有这些代码都可以替换为 Redux Toolkit 的 "RTK 查询" 数据获取和缓存层

¥All of that code can be replaced with Redux Toolkit's "RTK Query" data fetching and caching layer!

RTK 查询无需编写任何操作、thunk、reducers、选择器或效果来管理数据获取。(事实上,它实际上在内部使用所有相同的工具。)此外,RTK 查询还负责跟踪加载状态、删除重复请求以及管理缓存数据生命周期(包括删除不再需要的过期数据)。

¥RTK Query replaces the need to write any actions, thunks, reducers, selectors, or effects to manage data fetching. (In fact, it actually uses all those same tools internally.) Additionally, RTK Query takes care of tracking loading state, deduplicating requests, and managing cache data lifecycles (including removing expired data that is no longer needed).

要迁移,设置单个 RTK 查询 "API 切片" 定义并将生成的 reducer + 中间件添加到你的存储

¥To migrate, set up a single RTK Query "API slice" definition and add the generated reducer + middleware to your store:

src/features/api/apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const api = createApi({
baseQuery: fetchBaseQuery({
// Fill in your own server starting URL here
baseUrl: '/'
}),
endpoints: build => ({})
})
src/app/store.js
import { configureStore } from '@reduxjs/toolkit'

// Import the API object
import { api } from '../features/api/apiSlice'
// Import any other slice reducers as usual here
import usersReducer from '../features/users/usersSlice'

export const store = configureStore({
reducer: {
// Add the generated RTK Query "API slice" caching reducer
[api.reducerPath]: api.reducer,
// Add any other reducers
users: usersReducer
},
// Add the RTK Query API middleware
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(api.middleware)
})

然后,添加代表要获取和缓存的特定数据的 "endpoints",并为每个端点导出自动生成的 React hook:

¥Then, add "endpoints" that represents the specific data you want to fetch and cache, and export the auto-generated React hooks for each endpoint:

src/features/api/apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const api = createApi({
baseQuery: fetchBaseQuery({
// Fill in your own server starting URL here
baseUrl: '/'
}),
endpoints: build => ({
// A query endpoint with no arguments
getTodos: build.query({
query: () => '/todos'
}),
// A query endpoint with an argument
userById: build.query({
query: userId => `/users/${userId}`
}),
// A mutation endpoint
updateTodo: build.mutation({
query: updatedTodo => ({
url: `/todos/${updatedTodo.id}`,
method: 'POST',
body: updatedTodo
})
})
})
})

export const { useGetTodosQuery, useUserByIdQuery, useUpdateTodoMutation } = api

最后,在组件中使用钩子:

¥Finally, use the hooks in your components:

src/features/todos/TodoList.js
import { useGetTodosQuery } from '../api/apiSlice'

export function TodoList() {
const { data: todos, isFetching, isSuccess } = useGetTodosQuery()

// omit rendering logic here
}

使用 createAsyncThunk 获取数据

¥Data Fetching with createAsyncThunk

我们特别推荐使用 RTK 查询来获取数据。然而,一些用户告诉我们他们还没有准备好迈出这一步。在这种情况下,你至少可以使用 RTK 的 createAsyncThunk 减少一些手写 thunk 和 reducer 的样板。它会自动为你生成操作创建者和操作类型,调用你提供的异步函数来发出请求,并根据 Promise 生命周期分派这些操作。与 createAsyncThunk 相同的示例可能如下所示:

¥We specifically recommend using RTK Query for data fetching. However, some users have told us they aren't ready to make that step yet. In that case, you can at least cut down on some of the boilerplate of hand-written thunks and reducers using RTK's createAsyncThunk. It automatically generates the action creators and action types for you, calls the async function you provide to make the request, and dispatches those actions based on the promise lifecycle. The same example with createAsyncThunk might look like this:

src/features/todos/todosSlice
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import axios from 'axios'

const initialState = {
status: 'uninitialized',
todos: [],
error: null
}

const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
// Just make the async request here, and return the response.
// This will automatically dispatch a `pending` action first,
// and then `fulfilled` or `rejected` actions based on the promise.
// as needed based on the
const res = await axios.get('/todos')
return res.data
})

export const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// any additional "normal" case reducers here.
// these will generate new action creators
},
extraReducers: builder => {
// Use `extraReducers` to handle actions that were generated
// _outside_ of the slice, such as thunks or in other slices
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
// Pass the generated action creators to `.addCase()`
.addCase(fetchTodos.fulfilled, (state, action) => {
// Same "mutating" update syntax thanks to Immer
state.status = 'succeeded'
state.todos = action.payload
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed'
state.todos = []
state.error = action.error
})
}
})

export default todosSlice.reducer

你还需要编写任何选择器,并在 useEffect 钩子中自行调度 fetchTodos thunk。

¥You'd also still need to write any selectors, and dispatch the fetchTodos thunk yourself in a useEffect hook.

createListenerMiddleware 的反应逻辑

¥Reactive Logic with createListenerMiddleware

许多 Redux 应用都具有 "reactive" 风格的逻辑,用于监听特定操作或状态更改,并运行其他逻辑作为响应。这些行为通常使用 redux-sagaredux-observable 库来实现。

¥Many Redux apps have "reactive"-style logic that listens for specific actions or state changes, and runs additional logic in response. These behaviors are often implemented using the redux-saga or redux-observable libraries.

这些库用于多种任务。作为一个基本示例,监听操作、等待一秒钟,然后分派附加操作的传奇和史诗可能如下所示:

¥These libraries are used for a wide variety of tasks. As a basic example, a saga and an epic that listen for an action, wait one second, and then dispatch an additional action might look like this:

src/sagas/ping.js
import { delay, put, takeEvery } from 'redux-saga/effects'

export function* ping() {
yield delay(1000)
yield put({ type: 'PONG' })
}

// "Watcher" saga that waits for a "signal" action, which is
// dispatched only to kick off logic, not to update state
export function* pingSaga() {
yield takeEvery('PING', ping)
}
src/epics/ping.js
import { filter, mapTo } from 'rxjs/operators'
import { ofType } from 'redux-observable'

const pingEpic = action$ =>
action$.pipe(ofType('PING'), delay(1000), mapTo({ type: 'PONG' }))
src/app/store.js
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { combineEpics, createEpicMiddleware } from 'redux-observable';

// skip reducers

import { pingEpic } from '../sagas/ping'
import { pingSaga } from '../epics/ping

function* rootSaga() {
yield pingSaga()
}

const rootEpic = combineEpics(
pingEpic
);

const sagaMiddleware = createSagaMiddleware()
const epicMiddleware = createEpicMiddleware()

const middlewareEnhancer = applyMiddleware(sagaMiddleware, epicMiddleware)

const store = createStore(rootReducer, middlewareEnhancer)

sagaMiddleware.run(rootSaga)
epicMiddleware.run(rootEpic)

RTK "listener" 中间件旨在取代 sagas 和 observables,具有更简单的 API、更小的包大小和更好的 TS 支持。

¥The RTK "listener" middleware is designed to replace sagas and observables, with a simpler API, smaller bundle size, and better TS support.

传奇和史诗示例可以替换为监听器中间件,如下所示:

¥The saga and epic examples could be replaced with the listener middleware, like this:

src/app/listenerMiddleware.js
import { createListenerMiddleware } from '@reduxjs/toolkit'

// Best to define this in a separate file, to avoid importing
// from the store file into the rest of the codebase
export const listenerMiddleware = createListenerMiddleware()

export const { startListening, stopListening } = listenerMiddleware
src/features/ping/pingSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { startListening } from '../../app/listenerMiddleware'

const pingSlice = createSlice({
name: 'ping',
initialState,
reducers: {
pong(state, action) {
// state update here
}
}
})

export const { pong } = pingSlice.actions
export default pingSlice.reducer

// The `startListening()` call could go in different files,
// depending on your preferred app setup. Here, we just add
// it directly in a slice file.
startListening({
// Match this exact action type based on the action creator
actionCreator: pong,
// Run this effect callback whenever that action is dispatched
effect: async (action, listenerApi) => {
// Listener effect functions get a `listenerApi` object
// with many useful methods built in, including `delay`:
await listenerApi.delay(1000)
listenerApi.dispatch(pong())
}
})
src/app/store.js
import { configureStore } from '@reduxjs/toolkit'

import { listenerMiddleware } from './listenerMiddleware'

// omit reducers

export const store = configureStore({
reducer: rootReducer,
// Add the listener middleware _before_ the thunk or dev checks
middleware: getDefaultMiddleware =>
getDefaultMiddleware().prepend(listenerMiddleware.middleware)
})

迁移 TypeScript 以实现 Redux 逻辑

¥Migrating TypeScript for Redux Logic

使用 TypeScript 的旧版 Redux 代码通常遵循非常详细的模式来定义类型。特别是,社区中的许多用户决定为每个单独的操作手动定义 TS 类型,然后创建 "行动型联合",试图限制哪些特定操作实际上可以传递给 dispatch

¥Legacy Redux code that uses TypeScript typically follows very verbose patterns for defining types. In particular, many users in the community have decided to manually define TS types for each individual action, and then created "action type unions" that try to limit what specific actions can actually be passed to dispatch.

我们特别强烈建议不要使用这些模式!

¥We specifically and strongly recommend against these patterns!

src/actions/todos.ts
import { ADD_TODO, TOGGLE_TODO } from '../constants/todos'

// ❌ Common pattern: manually defining types for each action object
interface AddTodoAction {
type: typeof ADD_TODO
text: string
id: string
}

interface ToggleTodoAction {
type: typeof TOGGLE_TODO
id: string
}

// ❌ Common pattern: an "action type union" of all possible actions
export type TodoActions = AddTodoAction | ToggleTodoAction

export const addTodo = (id: string, text: string): AddTodoAction => ({
type: ADD_TODO,
text,
id
})

export const toggleTodo = (id: string): ToggleTodoAction => ({
type: TOGGLE_TODO,
id
})
src/reducers/todos.ts
import { ADD_TODO, TOGGLE_TODO, TodoActions } from '../constants/todos'

interface Todo {
id: string
text: string
completed: boolean
}

export type TodosState = Todo[]

const initialState: TodosState = []

export default function todosReducer(
state = initialState,
action: TodoActions
) {
switch (action.type) {
// omit reducer logic
default:
return state
}
}
src/app/store.ts
import { createStore, Dispatch } from 'redux'

import { TodoActions } from '../actions/todos'
import { CounterActions } from '../actions/counter'
import { TodosState } from '../reducers/todos'
import { CounterState } from '../reducers/counter'

// omit reducer setup

export const store = createStore(rootReducer)

// ❌ Common pattern: an "action type union" of all possible actions
export type RootAction = TodoActions | CounterActions
// ❌ Common pattern: manually defining the root state type with each field
export interface RootState {
todos: TodosState
counter: CounterState
}

// ❌ Common pattern: limiting what can be dispatched at the types level
export type AppDispatch = Dispatch<RootAction>

Redux Toolkit 旨在大大简化 TS 的使用,我们的建议包括尽可能多地推断类型!

¥Redux Toolkit is designed to drastically simplify TS usage, and our recommendations include inferring types as much as possible!

对于 我们的标准 TypeScript 设置和使用指南,首先设置存储文件以直接从存储本身推断 AppDispatchRootState 类型。这将正确地包括由中间件添加的对 dispatch 的任何修改,例如调度 thunk 的能力,以及在你修改切片的状态定义或添加更多切片时更新 RootState 类型的能力。

¥Per our standard TypeScript setup and usage guidelines, start with setting up the store file to infer AppDispatch and RootState types directly from the store itself. That will correctly include any modifications to dispatch that were added by middleware, such as the ability to dispatch thunks, and update the RootState type any time you modify a slice's state definition or add more slices.

app/store.ts
import { configureStore } from '@reduxjs/toolkit'
// omit any other imports

const store = configureStore({
reducer: {
todos: todosReducer,
counter: counterReducer
}
})

// Infer the `RootState` and `AppDispatch` types from the store itself

// Inferred state type: {todos: TodosState, counter: CounterState}
export type RootState = ReturnType<typeof store.getState>

// Inferred dispatch type: Dispatch & ThunkDispatch<RootState, undefined, UnknownAction>
export type AppDispatch = typeof store.dispatch

每个切片文件应该声明并导出其自己的切片状态的类型。然后,使用 PayloadAction 类型声明 createSlice.reducers 内任何 action 参数的类型。然后,生成的操作创建者还将具有他们接受的参数的正确类型以及他们返回的 action.payload 类型。

¥Each slice file should declare and export a type for its own slice state. Then, use the PayloadAction type to declare the type of any action argument inside of createSlice.reducers. The generated action creators will then also have the correct type for the argument they accept, and the type of action.payload that they return.

src/features/todos/todosSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface Todo {
id: string
text: string
completed: boolean
}

// Declare and export a type for the slice's state
export type TodosState = Todo[]

const initialState: TodosState = []

const todosSlice = createSlice({
name: 'todos',
// The `state` argument type will be inferred for all case reducers
// from the type of `initialState`
initialState,
reducers: {
// Use `PayloadAction<YourPayloadTypeHere>` for each `action` argument
todoAdded(state, action: PayloadAction<{ id: string; text: string }>) {
// omit logic
},
todoToggled(state, action: PayloadAction<string>) {
// omit logic
}
}
})

使用 React-Redux 实现 React 组件现代化

¥Modernizing React Components with React-Redux

迁移组件中 React-Redux 使用的一般方法是:

¥The general approach to migrating React-Redux usage in components is:

  • 将现有的 React 类组件迁移为函数组件

    ¥Migrate an existing React class component to be a function component

  • 使用组件内的 useSelectoruseDispatch 钩子替换 connect 封装器

    ¥Replace the connect wrapper with uses of the useSelector and useDispatch hooks inside the component

你可以针对每个组件单独执行此操作。带 connect 的组件和带钩子的组件可以同时共存。

¥You can do this on an individual per-component basis. Components with connect and with hooks can coexist at the same time.

本页不会介绍将类组件迁移到函数组件的过程,但将重点关注 React-Redux 特有的更改。

¥This page won't cover the process of migrating class components to function components, but will focus on the changes specific to React-Redux.

connect 迁移到 Hooks

¥Migrating connect to Hooks

使用 React-Redux 的 connect API 的典型旧版组件可能如下所示:

¥A typical legacy component using React-Redux's connect API might look like this:

src/features/todos/TodoListItem.js
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import {
todoToggled,
todoDeleted,
selectTodoById,
selectActiveTodoId
} from './todosSlice'

// A `mapState` function, possibly using values from `ownProps`,
// and returning an object with multiple separate fields inside
const mapStateToProps = (state, ownProps) => {
return {
todo: selectTodoById(state, ownProps.todoId),
activeTodoId: selectActiveTodoId(state)
}
}

// Several possible variations on how you might see `mapDispatch` written:

// 1) a separate function, manual wrapping of `dispatch`
const mapDispatchToProps = dispatch => {
return {
todoDeleted: id => dispatch(todoDeleted(id)),
todoToggled: id => dispatch(todoToggled(id))
}
}

// 2) A separate function, wrapping with `bindActionCreators`
const mapDispatchToProps2 = dispatch => {
return bindActionCreators(
{
todoDeleted,
todoToggled
},
dispatch
)
}

// 3) An object full of action creators
const mapDispatchToProps3 = {
todoDeleted,
todoToggled
}

// The component, which gets all these fields as props
function TodoListItem({ todo, activeTodoId, todoDeleted, todoToggled }) {
// rendering logic here
}

// Finished with the call to `connect`
export default connect(mapStateToProps, mapDispatchToProps)(TodoListItem)

使用 React-Redux hooks API,connect 调用和 mapState/mapDispatch 参数被钩子替换!

¥With the React-Redux hooks API, the connect call and mapState/mapDispatch arguments are replaced by hooks!

  • mapState 中返回的每个单独字段都成为单独的 useSelector 调用

    ¥Each individual field returned in mapState becomes a separate useSelector call

  • 通过 mapDispatch 传入的每个函数都成为组件内部定义的单独回调函数

    ¥Each function passed in via mapDispatch becomes a separate callback function defined inside the component

src/features/todos/TodoListItem.js
import { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
todoAdded,
todoToggled,
selectTodoById,
selectActiveTodoId
} from './todosSlice'

export function TodoListItem({ todoId }) {
// Get the actual `dispatch` function with `useDispatch`
const dispatch = useDispatch()

// Select values from the state with `useSelector`
const activeTodoId = useSelector(selectActiveTodoId)
// Use prop in scope to select a specific value
const todo = useSelector(state => selectTodoById(state, todoId))

// Create callback functions that dispatch as needed, with arguments
const handleToggleClick = () => {
dispatch(todoToggled(todoId))
}

const handleDeleteClick = () => {
dispatch(todoDeleted(todoId))
}

// omit rendering logic
}

一件不同的事情是,connect 通过阻止封装组件进行渲染来优化渲染性能,除非其传入的 stateProps+dispatchProps+ownProps 已更改。钩子无法做到这一点,因为它们位于组件内部。如果需要防止 React 的正常递归渲染行为,请自行将组件封装在 React.memo(MyComponent) 中。

¥One thing that's different is that connect optimized rendering performance by preventing the wrapped component from rendering unless its incoming stateProps+dispatchProps+ownProps had changed. The hooks cannot do that, since they're inside the component. If you need to prevent React's normal recursive rendering behavior, wrap the component in React.memo(MyComponent) yourself.

迁移组件的 TypeScript

¥Migrating TypeScript for Components

connect 的主要缺点之一是很难正确键入,并且类型声明最终变得非常冗长。这是因为它是一个高阶组件,而且它的 API 具有很大的灵活性(四个参数,全部可选,每个参数都有多个可能的重载和变体)。

¥One of the major downsides with connect is that it is very hard to type correctly, and the type declarations end up being extremely verbose. This is due to it being a Higher-Order Component, and also the amount of flexibility in its API (four arguments, all optional, each with multiple possible overloads and variations).

社区针对如何处理此问题提出了多种变体,其复杂程度各不相同。在低端,某些用法需要在 mapState() 中键入 state,然后计算组件的所有 props 的类型:

¥The community came up with multiple variations on how to handle this, with varying levels of complexity. On the low end, some usages required typing state in mapState(), and then calculating the types of all the props for the component:

Simple connect TS example
import { connect } from 'react-redux'
import { RootState } from '../../app/store'
import {
todoToggled,
todoDeleted,
selectTodoById,
selectActiveTodoId
} from './todosSlice'

interface TodoListItemOwnProps {
todoId: string
}

const mapStateToProps = (state: RootState, ownProps) => {
return {
todo: selectTodoById(state, ownProps.todoId),
activeTodoId: selectActiveTodoId(state)
}
}

const mapDispatchToProps = {
todoDeleted,
todoToggled
}

type TodoListItemProps = TodoListItemOwnProps &
ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps

function TodoListItem({
todo,
activeTodoId,
todoDeleted,
todoToggled
}: TodoListItemProps) {}

export default connect(mapStateToProps, mapDispatchToProps)(TodoListItem)

使用 typeof mapDispatch 作为对象尤其危险,因为如果包含 thunk 就会失败。

¥The use of typeof mapDispatch as an object in particular was dangerous, because it would fail if thunks were included.

其他社区创建的模式需要更多的开销,包括将 mapDispatch 声明为函数并调用 bindActionCreators 以传递 dispatch: Dispatch<RootActions> 类型,或者手动计算封装组件接收的所有 props 的类型并将它们作为泛型传递给 connect

¥Other community-created patterns required significantly more overhead, including declaring mapDispatch as a function and calling bindActionCreators in order to pass through a dispatch: Dispatch<RootActions> type, or manually calculating the types of all the props received by the wrapped component and passing those as generics to connect.

一个稍微好一点的替代方案是在 v7.x 中添加到 @types/react-reduxConnectedProps<T> 类型,它可以推断将从 connect 传递到组件的所有 props 的类型。这确实需要将对 connect 的调用分成两部分,以便推断正常工作:

¥One slightly-better alternative was the ConnectedProps<T> type that was added to @types/react-redux in v7.x, which enabled inferring the type of all the props that would be passed to the component from connect. This did require splitting up the call to connect into two parts for the inference to work right:

ConnectedProps<T> TS example
import { connect, ConnectedProps } from 'react-redux'
import { RootState } from '../../app/store'
import {
todoToggled,
todoDeleted,
selectTodoById,
selectActiveTodoId
} from './todosSlice'

interface TodoListItemOwnProps {
todoId: string
}

const mapStateToProps = (state: RootState, ownProps) => {
return {
todo: selectTodoById(state, ownProps.todoId),
activeTodoId: selectActiveTodoId(state)
}
}

const mapDispatchToProps = {
todoDeleted,
todoToggled
}

// Call the first part of `connect` to get the function that accepts the component.
// This knows the types of the props returned by `mapState/mapDispatch`
const connector = connect(mapStateToProps, mapDispatchToProps)
// The `ConnectedProps<T> util type can extract "the type of all props from Redux"
type PropsFromRedux = ConnectedProps<typeof connector>

// The final component props are "the props from Redux" + "props from the parent"
type TodoListItemProps = PropsFromRedux & TodoListItemOwnProps

// That type can then be used in the component
function TodoListItem({
todo,
activeTodoId,
todoDeleted,
todoToggled
}: TodoListItemProps) {}

// And the final wrapped component is generated and exported
export default connector(TodoListItem)

React-Redux hooks API 与 TypeScript 一起使用要简单得多!钩子不是处理组件封装、类型推断和泛型层,而是接受参数并返回结果的简单函数。你需要传递的只是 RootStateAppDispatch 的类型。

¥The React-Redux hooks API is much simpler to use with TypeScript! Instead of dealing with layers of component wrapping, type inference, and generics, the hooks are simple functions that take arguments and return a result. All that you need to pass around are the types for RootState and AppDispatch.

根据 我们的标准 TypeScript 设置和使用指南,我们专门教授为钩子设置 "pre-typed" 别名,以便这些钩子具有正确的类型,并且仅在应用中使用那些预先键入的钩子。

¥Per our standard TypeScript setup and usage guidelines, we specifically teach setting up "pre-typed" aliases for the hooks, so that those have the correct types baked in, and only use those pre-typed hooks in the app.

首先,设置钩子:

¥First, set up the hooks:

src/app/hooks.ts
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()

然后,在你的组件中使用它们:

¥Then, use them in your components:

src/features/todos/TodoListItem.tsx
import { useAppSelector, useAppDispatch } from '../../app/hooks'
import {
todoToggled,
todoDeleted,
selectTodoById,
selectActiveTodoId
} from './todosSlice'

interface TodoListItemProps {
todoId: string
}

function TodoListItem({ todoId }: TodoListItemProps) {
// Use the pre-typed hooks in the component
const dispatch = useAppDispatch()
const activeTodoId = useAppSelector(selectActiveTodoId)
const todo = useAppSelector(state => selectTodoById(state, todoId))

// omit event handlers and rendering logic
}

更多信息

¥Further Information

有关更多详细信息,请参阅这些文档页面和博客文章:

¥See these docs pages and blog posts for more details: