跳到主要内容

迁移到现代 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 及其关联的 actions。用 RTK 的 createSlice 替换它们。每次替换一个 reducer。
  • 根据需要,用 RTK Query 或 createAsyncThunk 替换现有的数据获取逻辑
  • 根据需要使用 RTK 的其他 API,如 createListenerMiddlewarecreateEntityAdapter

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

信息

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

使用 configureStore 设置 Store

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

  • 将 slice reducer 合并到 root 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'

// 自动添加 thunk 中间件和 Redux DevTools 扩展
const store = configureStore({
// 自动调用 `combineReducers`
reducer: {
posts: postsReducer,
users: usersReducer,
},
})

这个对 configureStore 的一次调用为我们做了所有的工作:

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

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

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

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

  • 单独合并 reducer(由于其他架构约束有时需要)
  • 添加额外的中间件,无论是有条件的还是无条件的
  • 将"额外参数"传入 thunk 中间件,如 API 服务层
  • 使用 Redux-Persist 库,该库需要对其非序列化的 action 类型进行特殊处理
  • 在生产环境中关闭 devtools,在开发环境中设置额外的 devtools 选项

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

自定义 Store 设置: 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'

// 如果需要,可以自己调用 `combineReducers`
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({
// 可以单独创建一个 root reducer 并传入
reducer: rootReducer,
middleware: (getDefaultMiddleware) => {
const middleware = getDefaultMiddleware({
// 向 thunk 中间件传入一个自定义的 `extra` 参数
thunk: {
extraArgument: { serviceLayer },
},
// 自定义内置的序列化 dev 检查
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(customMiddleware, api.middleware)

// 在 dev 中有条件地添加另一个中间件
if (process.env.NODE_ENV !== 'production') {
middleware.push(logger)
}

return middleware
},
// 在生产环境中关闭 devtools,或在开发环境中传入选项
devTools:
process.env.NODE_ENV === 'production'
? false
: {
stateSanitizer: stateSanitizerForDevtools,
},
})

使用 createSlice 的 Reducers 和 Actions

典型的传统 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 旨在消除编写 reducers、actions 和不可变更新的所有 "样板代码"!

使用 Redux Toolkit,对传统代码有多个更改:

  • createSlice 将完全消除手写的 action 创建者和 action 类型
  • 所有像 action.textaction.id 这样的独特命名字段都被 action.payload 替换,作为一个单独的值或包含这些字段的对象
  • 手写的不可变更新被 reducers 中的 "变异" 逻辑替换,感谢 Immer
  • 不需要为每种类型的代码分别创建文件
  • 我们教授在一个 "slice" 文件中有一个给定 reducer 的 所有 逻辑
  • 我们建议按 "特性" 组织文件,而不是按 "代码类型",与相关代码生活在同一个文件夹中
  • 理想情况下,reducers 和 actions 的命名应使用过去时,并描述 "发生的事情",而不是祈使 "现在做这件事",例如 todoAdded 而不是 ADD_TODO

这些常量、actions 和 reducers 的单独文件都将被一个 "slice" 文件替换。现代化的 slice 文件看起来像这样:

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

const initialState = []

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// 给 case reducers 有意义的过去时 "事件" 风格的名字
todoAdded(state, action) {
const { id, text } = action.payload
// 感谢 Immer 的 "变异" 更新语法,不需要 `return`
state.todos.push({
id,
text,
completed: false,
})
},
todoToggled(state, action) {
// 寻找特定的嵌套对象进行更新。
// 在这种情况下,`action.payload` 是 action 中的默认字段,
// 可以包含 `id` 值 - 不需要单独的 `action.id`
const matchingTodo = state.todos.find(
(todo) => todo.id === action.payload,
)

if (matchingTodo) {
// 可以直接 "变异" 嵌套对象
matchingTodo.completed = !matchingTodo.completed
}
},
},
})

// `createSlice` 自动生成了这些名字的 action 创建者。
// 从这个 "slice" 文件中导出它们作为命名导出
export const { todoAdded, todoToggled } = todosSlice.actions

// 将 slice reducer 作为默认导出
export default todosSlice.reducer

当你调用 dispatch(todoAdded('Buy milk')),你传递给 todoAdded action 创建者的任何单一值都会自动用作 action.payload 字段。如果你需要传递多个值,可以作为一个对象传递,如 dispatch(todoAdded({id, text}))。或者,你可以使用 createSlice reducer 中的 "prepare" 符号 来接受多个单独的参数并创建 payload 字段。prepare 符号也适用于 action 创建者需要做额外工作的情况,例如为每个项目生成唯一的 ID。

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

使用 RTK Query 进行数据获取

在 React+Redux 应用中,典型的传统数据获取需要许多移动的部分和类型的代码:

  • 表示 "请求开始"、"请求成功" 和 "请求失败" 动作的 action 创建者和 action 类型
  • 分发动作并进行异步请求的 Thunks
  • 跟踪加载状态和存储缓存数据的 reducers
  • 从 store 中读取这些值的 selectors
  • 在组件挂载后分发 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 是常见的,但也可以是 `fetch`,或者你自己的 "API 服务" 层
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])

// 省略渲染逻辑
}

许多用户可能正在使用 redux-saga 库来管理数据获取,在这种情况下,他们可能有 额外的 "信号" 动作类型用于触发 sagas,以及这个 saga 文件而不是 thunks:

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

// 实际获取数据的 Saga
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))
}
}

// "观察者" saga,等待一个 "信号" 动作,该动作只是
// 用于启动逻辑,而不是更新状态
export function* fetchTodosSaga() {
yield takeEvery(FETCH_TODOS_BEGIN, fetchTodos)
}

所有 的这些代码都可以用 Redux Toolkit 的 "RTK Query" 数据获取和缓存层 来替换!

RTK Query 取代了编写 任何 动作、thunks、reducers、selectors 或 effects 来管理数据获取的需要。(实际上,它内部实际上 使用 所有这些相同的工具。)此外,RTK Query 负责跟踪加载状态,消除重复的请求,并管理缓存数据生命周期(包括删除不再需要的过期数据)。

要迁移,设置一个单一的 RTK Query "API slice" 定义并将生成的 reducer + middleware 添加到你的 store

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

export const api = createApi({
baseQuery: fetchBaseQuery({
// 在这里填写你自己的服务器起始 URL
baseUrl: '/',
}),
endpoints: (build) => ({}),
})
src/app/store.js
import { configureStore } from '@reduxjs/toolkit'

// 导入 API 对象
import { api } from '../features/api/apiSlice'
// 正常导入任何其他 slice reducers
import usersReducer from '../features/users/usersSlice'

export const store = configureStore({
reducer: {
// 添加生成的 RTK Query "API slice" 缓存 reducer
[api.reducerPath]: api.reducer,
// 添加任何其他 reducers
users: usersReducer,
},
// 添加 RTK Query API middleware
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
})

然后,添加代表你想要获取和缓存的特定数据的 "endpoints",并导出每个 endpoint 的自动生成的 React hooks:

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

export const api = createApi({
baseQuery: fetchBaseQuery({
// 在这里填写你自己的服务器起始 URL
baseUrl: '/',
}),
endpoints: (build) => ({
// 一个没有参数的查询 endpoint
getTodos: build.query({
query: () => '/todos',
}),
// 一个有参数的查询 endpoint
userById: build.query({
query: (userId) => `/users/${userId}`,
}),
// 一个 mutation endpoint
updateTodo: build.mutation({
query: (updatedTodo) => ({
url: `/todos/${updatedTodo.id}`,
method: 'POST',
body: updatedTodo,
}),
}),
}),
})

export const { useGetTodosQuery, useUserByIdQuery, useUpdateTodoMutation } = api

最后,在你的组件中使用 hooks:

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

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

// 省略渲染逻辑
}

使用 createAsyncThunk 进行数据获取

**我们 特别 推荐使用 RTK Query 进行数据获取。**然而,一些用户告诉我们他们还没有准备好迈出这一步。在这种情况下,你至少可以使用 RTK 的 createAsyncThunk 减少一些手写 thunks 和 reducers 的样板代码。它会自动为你生成 action creators 和 action types,调用你提供的异步函数进行请求,并根据 promise 的生命周期分发这些 action。使用 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 () => {
// 在这里进行异步请求,并返回响应。
// 这将自动先分发一个 `pending` action,
// 然后根据 promise 分发 `fulfilled` 或 `rejected` action。
// 根据需要进行
const res = await axios.get('/todos')
return res.data
})

export const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// 在这里添加任何额外的 "正常" case reducers。
// 这些将生成新的 action creators
},
extraReducers: (builder) => {
// 使用 `extraReducers` 来处理在 slice 外部生成的 action,
// 如 thunks 或在其他 slices 中
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
// 将生成的 action creators 传递给 `.addCase()`
.addCase(fetchTodos.fulfilled, (state, action) => {
// 同样的 "变异" 更新语法,感谢 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。

使用 createListenerMiddleware 的反应式逻辑

许多 Redux 应用程序有 "反应式" 风格的逻辑,它监听特定的动作或状态变化,并作出额外的逻辑响应。这些行为通常使用 redux-sagaredux-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' })
}

// "观察者" saga,等待一个 "信号" 动作,该动作只是
// 用于启动逻辑,而不是更新状态
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';

// 跳过 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" 中间件旨在用更简单的 API、更小的包大小和更好的 TS 支持来替代 sagas 和 observables。

saga 和 epic 的例子可以用 listener 中间件替换,像这样:

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

// 最好在一个单独的文件中定义这个,以避免从 store 文件导入到代码库的其余部分
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) {
// 在这里更新状态
},
},
})

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

// `startListening()` 的调用可以放在不同的文件中,
// 这取决于你喜欢的应用程序设置。在这里,我们直接在
// slice 文件中添加它。
startListening({
// 根据 action creator 精确匹配这个 action 类型
actionCreator: pong,
// 每当该动作被分发时,运行这个 effect 回调
effect: async (action, listenerApi) => {
// Listener effect 函数获取一个 `listenerApi` 对象
// 内置了许多有用的方法,包括 `delay`:
await listenerApi.delay(1000)
listenerApi.dispatch(pong())
},
})
src/app/store.js
import { configureStore } from '@reduxjs/toolkit'

import { listenerMiddleware } from './listenerMiddleware'

// 省略 reducers

export const store = configureStore({
reducer: rootReducer,
// 在 thunk 或 dev 检查之前添加 listener 中间件
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().prepend(listenerMiddleware.middleware),
})

迁移 Redux 逻辑的 TypeScript

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

我们特别且强烈建议_反对_这些模式!

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

// ❌ 常见模式:为每个操作对象手动定义类型
interface AddTodoAction {
type: typeof ADD_TODO
text: string
id: string
}

interface ToggleTodoAction {
type: typeof TOGGLE_TODO
id: string
}

// ❌ 常见模式:所有可能操作的"操作类型联合"
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) {
// 省略 reducer 逻辑
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'

// 省略 reducer 设置

export const store = createStore(rootReducer)

// ❌ 常见模式:所有可能操作的"操作类型联合"
export type RootAction = TodoActions | CounterActions
// ❌ 常见模式:手动定义每个字段的根状态类型
export interface RootState {
todos: TodosState
counter: CounterState
}

// ❌ 常见模式:在类型级别限制可以分派的内容
export type AppDispatch = Dispatch<RootAction>

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

根据我们的标准 TypeScript 设置和使用指南,首先设置 store 文件,直接从 store 本身推断 AppDispatchRootState 类型。这将正确地包括由中间件添加到 dispatch 的任何修改,例如分派 thunks 的能力,并在您修改切片的状态定义或添加更多切片时更新 RootState 类型。

app/store.ts
import { configureStore } from '@reduxjs/toolkit'
// 省略任何其他导入

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

// 从 store 本身推断 `RootState` 和 `AppDispatch` 类型

// 推断的状态类型:{todos: TodosState, counter: CounterState}
export type RootState = ReturnType<typeof store.getState>

// 推断的 dispatch 类型:Dispatch & ThunkDispatch<RootState, undefined, UnknownAction>
export type AppDispatch = typeof store.dispatch

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

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

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

// 声明并导出切片状态的类型
export type TodosState = Todo[]

const initialState: TodosState = []

const todosSlice = createSlice({
name: 'todos',
// 所有 case reducers 的 `state` 参数类型将从 `initialState` 的类型推断
initialState,
reducers: {
// 对于每个 `action` 参数,使用 `PayloadAction<YourPayloadTypeHere>`
todoAdded(state, action: PayloadAction<{ id: string; text: string }>) {
// 省略逻辑
},
todoToggled(state, action: PayloadAction<string>) {
// 省略逻辑
},
},
})

使用 React-Redux 现代化 React 组件

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

  • 将现有的 React 类组件迁移到函数组件
  • useSelectoruseDispatch 钩子的使用替换 connect 包装器_在_组件内部

您可以在每个组件的基础上进行此操作。带有 connect 和带有钩子的组件可以同时共存。

此页面不会涵盖将类组件迁移到函数组件的过程,但会关注特定于 React-Redux 的更改。

connect 迁移到 Hooks

一个典型的使用 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'

// 一个 `mapState` 函数,可能使用来自 `ownProps` 的值,
// 并返回一个包含多个独立字段的对象
const mapStateToProps = (state, ownProps) => {
return {
todo: selectTodoById(state, ownProps.todoId),
activeTodoId: selectActiveTodoId(state),
}
}

// 你可能会看到 `mapDispatch` 的几种可能写法:

// 1) 一个单独的函数,手动包装 `dispatch`
const mapDispatchToProps = (dispatch) => {
return {
todoDeleted: (id) => dispatch(todoDeleted(id)),
todoToggled: (id) => dispatch(todoToggled(id)),
}
}

// 2) 一个单独的函数,使用 `bindActionCreators` 包装
const mapDispatchToProps2 = (dispatch) => {
return bindActionCreators(
{
todoDeleted,
todoToggled,
},
dispatch,
)
}

// 3) 一个充满 action 创建者的对象
const mapDispatchToProps3 = {
todoDeleted,
todoToggled,
}

// 这个组件,它获取所有这些字段作为 props
function TodoListItem({ todo, activeTodoId, todoDeleted, todoToggled }) {
// 渲染逻辑在这里
}

// 用 `connect` 调用结束
export default connect(mapStateToProps, mapDispatchToProps)(TodoListItem)

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

  • 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 }) {
// 使用 `useDispatch` 获取实际的 `dispatch` 函数
const dispatch = useDispatch()

// 使用 `useSelector` 从状态中选择值
const activeTodoId = useSelector(selectActiveTodoId)
// 使用作用域中的 prop 来选择特定的值
const todo = useSelector((state) => selectTodoById(state, todoId))

// 创建需要时可以 dispatch 的回调函数,带有参数
const handleToggleClick = () => {
dispatch(todoToggled(todoId))
}

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

// 省略渲染逻辑
}

有一点不同的是,connect 通过防止包装组件渲染来优化渲染性能,除非其传入的 stateProps+dispatchProps+ownProps 发生了变化。Hooks 不能做到这一点,因为它们在组件的 内部 。如果你需要防止 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 作为对象是危险的,因为如果包含了 thunks,它会失败。

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

一个稍好的替代方案是在 @types/react-redux 的 v7.x 中添加的 ConnectedProps<T> 类型,它可以推断出 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,
}

// 调用 `connect` 的第一部分以获取接受组件的函数。
// 这知道 `mapState/mapDispatch` 返回的 props 的类型
const connector = connect(mapStateToProps, mapDispatchToProps)
// `ConnectedProps<T>` 工具类型可以提取 "来自 Redux 的所有 props 的类型"
type PropsFromRedux = ConnectedProps<typeof connector>

// 最终的组件 props 是 "来自 Redux 的 props" + "来自父组件的 props"
type TodoListItemProps = PropsFromRedux & TodoListItemOwnProps

// 然后可以在组件中使用该类型
function TodoListItem({
todo,
activeTodoId,
todoDeleted,
todoToggled,
}: TodoListItemProps) {}

// 最后生成并导出包装后的组件
export default connector(TodoListItem)

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

根据我们的标准 TypeScript 设置和使用指南,我们特别教授设置 "预类型化" 的 hooks 别名,这样这些 hooks 就有了正确的类型,并且只在应用程序中使用这些预类型化的 hooks。

首先,设置 hooks:

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

// 在你的应用程序中使用,而不是普通的 `useDispatch` 和 `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) {
// 在组件中使用预类型化的 hooks
const dispatch = useAppDispatch()
const activeTodoId = useAppSelector(selectActiveTodoId)
const todo = useAppSelector((state) => selectTodoById(state, todoId))

// 省略事件处理器和渲染逻辑
}

更多信息

查看这些文档页面和博客文章以获取更多详细信息: