Setup > Migrating to Modern Redux: how to modernize legacy Redux code">Setup > Migrating to Modern Redux: how to modernize legacy Redux code">
跳至主要内容

迁移到现代 Redux

您将学到什么
  • 如何将传统的“手写”Redux 逻辑现代化以使用 Redux Toolkit
  • 如何将传统的 React-Redux connect 组件现代化为使用 hooks API
  • 如何将使用 TypeScript 的 Redux 逻辑和 React-Redux 组件现代化

概述

Redux 自 2015 年问世以来,我们编写 Redux 代码的推荐模式已经发生了重大变化。就像 React 从 createClass 发展到 React.Component 再到带有 hooks 的函数组件一样,Redux 也从手动设置 store + 手写 reducer(使用对象扩展)+ React-Redux 的 connect,发展到 Redux Toolkit 的 configureStore + createSlice + React-Redux 的 hooks API。

许多用户正在使用比这些“现代 Redux”模式出现时间更早的旧 Redux 代码库。将这些代码库迁移到当今推荐的现代 Redux 模式将导致代码库更小且更易于维护。

好消息是,您可以逐步、逐段地将代码迁移到现代 Redux,旧的和新的 Redux 代码可以共存并协同工作!

本页介绍了您可以用来现代化现有传统 Redux 代码库的一般方法和技术。

信息

有关使用 Redux Toolkit + React-Redux hooks 的“现代 Redux”如何简化使用 Redux 的更多详细信息,请参阅以下其他资源

使用 Redux Toolkit 现代化 Redux 逻辑

迁移 Redux 逻辑的一般方法是

  • 用 Redux Toolkit 的 configureStore 替换现有的手动 Redux store 设置
  • 选择一个现有的 slice reducer 及其关联的操作。用 RTK 的 createSlice 替换它们。一次替换一个 reducer。
  • 根据需要,用 RTK Query 或 createAsyncThunk 替换现有的数据获取逻辑
  • 根据需要使用 RTK 的其他 API,如 createListenerMiddlewarecreateEntityAdapter

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

信息

您可以在 Redux Fundamentals, 第 8 部分:使用 Redux Toolkit 的现代 Redux 中看到这种通用方法的实际应用。

使用 configureStore 设置 Store

典型的传统 Redux store 设置文件会执行几个不同的步骤

  • 将切片 reducer 合并到根 reducer 中
  • 创建中间件增强器,通常使用 thunk 中间件,以及可能在开发模式下使用的其他中间件,例如 redux-logger
  • 添加 Redux DevTools 增强器,并将增强器组合在一起
  • 调用 createStore

以下是这些步骤在现有应用程序中的可能样子

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 的一次调用来替换.

RTK 的 configureStore 围绕原始的 createStore 方法进行包装,并自动为我们处理大部分 store 设置。实际上,我们可以将其简化为有效的一步

基本 Store 设置: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 的一次调用就为我们完成了所有工作

  • 它调用 combineReducerspostsReducerusersReducer 合并到根 reducer 函数中,该函数将处理看起来像 {posts, users} 的根状态
  • 它调用 createStore 使用该根 reducer 创建 Redux store
  • 它自动添加了 thunk 中间件并调用了 applyMiddleware
  • 它自动添加了更多中间件来检查常见的错误,例如意外地修改状态
  • 它自动设置了 Redux DevTools Extension 连接

如果您的 store 设置需要额外的步骤,例如添加额外的中间件,将 extra 参数传递给 thunk 中间件,或创建持久化的根 reducer,您也可以这样做。以下是一个更大的示例,展示了自定义内置中间件和启用 Redux-Persist,这演示了一些使用 configureStore 的选项

详细示例:带有持久性和中间件的自定义 Store 设置

此示例展示了设置 Redux store 时可能遇到的几个常见任务

  • 分别组合 reducer(有时由于其他架构约束而需要)
  • 添加额外的中间件,包括条件和无条件添加
  • 将“额外参数”传递到 thunk 中间件,例如 API 服务层
  • 使用 Redux-Persist 库,该库需要对不可序列化的操作类型进行特殊处理
  • 在生产环境中关闭 devtools,并在开发环境中设置额外的 devtools 选项

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

自定义商店设置: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({
// Can create a root reducer separately and pass that in
reducer: rootReducer,
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 和 Action

一个典型的传统 Redux 代码库将它的 reducer 逻辑、action 创建器和 action 类型分散在不同的文件中,这些文件通常按类型放在不同的文件夹中。reducer 逻辑使用 switch 语句和手动编写的不可变更新逻辑(使用对象扩展和数组映射)编写

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、action 和不可变更新的所有“样板代码”!

使用 Redux Toolkit,对传统代码进行了多项更改

  • createSlice 将完全消除手动编写的 action 创建器和 action 类型
  • 所有唯一命名的字段(如 action.textaction.id)将被 action.payload 替换,可以是单个值,也可以是包含这些字段的对象
  • 手动编写的不可变更新将被 reducer 中的“可变”逻辑替换,这得益于 Immer
  • 无需为每种类型的代码创建单独的文件
  • 我们建议将给定 reducer 的所有逻辑都放在一个“切片”文件中
  • 我们建议不要按“代码类型”创建单独的文件夹,而是按“功能”组织文件,将相关代码放在同一个文件夹中
  • 理想情况下,reducer 和 action 的命名应该使用过去时,描述“发生的事情”,而不是命令式的“现在做这件事”,例如 todoAdded 而不是 ADD_TODO

用于常量、action 和 reducer 的那些单独文件将被一个“切片”文件替换。现代化的切片文件看起来像这样

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 创建器的任何单个值都会自动用作 action.payload 字段。如果你需要传递多个值,请以对象的形式传递,例如 dispatch(todoAdded({id, text}))。或者,你可以使用 createSlice reducer 中的“准备”符号 来接受多个单独的参数并创建 payload 字段。“准备”符号在 action 创建器执行额外工作(例如为每个项目生成唯一 ID)的情况下也很有用。

虽然 Redux Toolkit 不强制要求特定的文件夹和文件结构或操作命名,但我们推荐这些最佳实践,因为我们发现它们能带来更易于维护和理解的代码。

使用 RTK Query 进行数据获取

在传统的 React+Redux 应用中,典型的数据获取需要许多不同的代码片段和类型。

  • 代表“请求开始”、“请求成功”和“请求失败”操作的动作创建器和动作类型。
  • 用于分发操作并进行异步请求的 Thunk。
  • 用于跟踪加载状态和存储缓存数据的 Reducer。
  • 用于从 Store 中读取这些值的 Selector。
  • 在组件挂载后分发 Thunk,可以通过类组件中的 componentDidMount 或函数组件中的 useEffect 来实现。

这些代码通常会分散在多个不同的文件中。

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 库来管理数据获取,在这种情况下,他们可能会有额外的“信号”动作类型用于触发 Saga,以及这个 Saga 文件而不是 Thunk。

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 Query”数据获取和缓存层所取代!

RTK Query 替代了编写任何操作、Thunk、Reducer、Selector 或效果来管理数据获取的需要。(实际上,它在内部使用了所有这些工具。)此外,RTK Query 还负责跟踪加载状态、对请求进行去重以及管理缓存数据的生命周期(包括删除不再需要的过期数据)。

要迁移,设置一个单独的 RTK Query“API 切片”定义,并将生成的 Reducer + 中间件添加到你的 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),
})

然后,添加代表你想要获取和缓存的特定数据的“端点”,并导出每个端点的自动生成的 React Hook。

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

最后,在你的组件中使用这些 Hook。

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

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

// omit rendering logic here
}

使用 createAsyncThunk 进行数据获取

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

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

你仍然需要编写任何 Selector,并在 useEffect Hook 中自己分发 fetchTodos Thunk。

使用 createListenerMiddleware 进行响应式逻辑

许多 Redux 应用程序具有“响应式”风格的逻辑,这些逻辑会监听特定的动作或状态变化,并相应地运行额外的逻辑。这些行为通常使用 `redux-saga` 或 `redux-observable` 库实现。

这些库用于各种各样的任务。作为一个基本示例,一个监听动作、等待一秒钟,然后分发额外动作的 saga 和 epic 可能看起来像这样

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 的“监听器”中间件旨在用更简单的 API、更小的包大小和更好的 TS 支持来替代 saga 和 observable。

saga 和 epic 示例可以用监听器中间件替换,如下所示

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),
})

迁移用于 Redux 逻辑的 TypeScript

使用 TypeScript 的传统 Redux 代码通常遵循为定义类型而制定的非常冗长的模式。特别是,社区中的许多用户决定为每个单独的动作手动定义 TS 类型,然后创建“动作类型联合”,试图限制实际可以传递给 `dispatch` 的特定动作。

我们明确强烈建议不要使用这些模式!

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 的使用,我们的建议包括尽可能推断类型!

根据我们标准的 TypeScript 设置和使用指南,从设置 store 文件开始,直接从 store 本身推断 `AppDispatch` 和 `RootState` 类型。这将正确地包含由中间件添加的任何对 `dispatch` 的修改,例如分发 thunk 的能力,并在每次修改切片的 state 定义或添加更多切片时更新 `RootState` 类型。

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

每个切片文件都应该声明并导出其自身切片 state 的类型。然后,使用 `PayloadAction` 类型来声明 `createSlice.reducers` 中任何 `action` 参数的类型。生成的 action 创建者也将具有它们接受的参数的正确类型,以及它们返回的 `action.payload` 的类型。

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 组件

迁移组件中 React-Redux 用法的通用方法是

  • 将现有的 React 类组件迁移为函数组件
  • useSelectoruseDispatch 钩子的使用来替换 connect 包装器,这些钩子位于组件内部

您可以逐个组件地进行此操作。使用 connect 和钩子的组件可以同时共存。

本页不会介绍将类组件迁移为函数组件的过程,而是重点介绍 React-Redux 的特定更改。

connect 迁移到钩子

使用 React-Redux 的 connect API 的典型遗留组件可能如下所示

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 钩子 API,connect 调用和 mapState/mapDispatch 参数被钩子替换!

  • mapState 中返回的每个单独字段都成为一个单独的 useSelector 调用
  • 通过 mapDispatch 传递的每个函数都成为在组件内部定义的单独回调函数
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) 中。

迁移组件的 TypeScript

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

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

简单的 connect TS 示例
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 时会失败。

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

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

ConnectedProps<T> TS 示例
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 中使用起来非常简单!与处理组件包装、类型推断和泛型的层层嵌套不同,hooks 是简单的函数,它们接受参数并返回结果。您只需要传递RootStateAppDispatch的类型。

根据我们标准的 TypeScript 设置和使用指南,我们专门教授为 hooks 设置“预类型化”别名,以便它们具有正确的类型,并且只在应用程序中使用这些预类型化的 hooks。

首先,设置 hooks

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

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

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

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
}

更多信息

请参阅这些文档页面和博客文章以获取更多详细信息