迁移至现代 Redux
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
- 如何将传统"手写"的 Redux 逻辑现代化改造为使用 Redux Toolkit
- 如何将传统的 React-Redux
connect组件现代化改造为使用 Hooks API - 如何现代化改造使用 TypeScript 的 Redux 逻辑和 React-Redux 组件
概述
Redux 自 2015 年诞生以来,我们推荐的代码编写模式已发生显著变化。正如 React 从 createClass 演进到 React.Component 再到带 Hooks 的函数组件,Redux 也从手动创建 store + 对象展开符手写 reducers + React-Redux 的 connect,演进为 Redux Toolkit 的 configureStore + createSlice + React-Redux 的 Hooks API。
许多开发者仍在维护在这些"现代 Redux"模式出现前创建的旧代码库。将这些代码库迁移至当今推荐的现代 Redux 模式,将大幅减小代码体积并提升可维护性。
好消息是:你可以逐步将代码迁移至现代 Redux,新旧代码可以共存并协同工作!
本文概述了现代化改造现有传统 Redux 代码库的通用方法和技术。
关于 Redux Toolkit + React-Redux Hooks 如何简化 Redux 使用的更多细节,请参阅:
使用 Redux Toolkit 现代化 Redux 逻辑
迁移 Redux 逻辑的通用方法是:
-
使用 Redux Toolkit 的
configureStore替换现有手动 Redux store 配置 -
选择现有切片 reducer 及其关联 action,使用 RTK 的
createSlice替换它们。每次仅处理一个 reducer -
根据需要,使用 RTK Query 或
createAsyncThunk替换现有数据获取逻辑 -
按需使用 RTK 的其他 API(如
createListenerMiddleware或createEntityAdapter)
你应始终从替换传统的 createStore 调用为 configureStore 开始。这是一次性步骤,所有现有 reducers 和中间件将继续正常工作。configureStore 包含针对常见错误(如意外变更状态和不可序列化值)的开发模式检查,这有助于识别代码库中存在此类问题的区域。
你可以在Redux 基础:第八部分 - 使用 Redux Toolkit 的现代 Redux中查看此方法的实际应用。
使用 configureStore 配置 Store
典型的传统 Redux store 配置文件包含多个步骤:
-
将切片 reducers 组合为根 reducer
-
创建中间件增强器(通常包含 thunk 中间件),开发模式下可能包含其他中间件(如
redux-logger) -
添加 Redux DevTools 增强器并组合所有增强器
-
调用
createStore
现有应用中的这些步骤可能如下所示:
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 配置工作。实际上,我们可以将其简化为一步操作:
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 调用完成了所有工作:
-
它自动调用
combineReducers将postsReducer和usersReducer合并为根 reducer 函数,该函数将处理形如{posts, users}的根状态 -
它调用
createStore创建使用该根 reducer 的 Redux store -
它自动添加 thunk 中间件并调用
applyMiddleware -
自动添加额外中间件检查常见错误(如意外变更状态)
-
自动配置 Redux DevTools 扩展连接
如果您的 store 配置需要额外步骤,例如添加额外中间件、向 thunk 中间件传递 extra 参数或创建持久化根 reducer,也可以实现。以下示例展示了如何自定义内置中间件并启用 Redux-Persist,演示了 configureStore 的部分配置选项:
Detailed Example: Custom Store Setup with Persistence and Middleware
This example shows several possible common tasks when setting up a Redux store:
- Combining the reducers separately (sometimes needed due to other architectural constraints)
- Adding additional middleware, both conditionally and unconditionally
- Passing an "extra argument" into the thunk middleware, such as an API service layer
- 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.
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 处理 Reducers 和 Actions
典型的传统 Redux 代码库将其 reducer 逻辑、action 创建器和 action 类型分散在不同文件中,这些文件通常按类型存放在独立文件夹中。Reducer 逻辑使用 switch 语句编写,并通过对象展开和数组映射手动实现不可变更新:
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
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
})
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.text和action.id)被替换为action.payload(可以是单个值或包含这些字段的对象) -
借助 Immer,手动编写的不可变更新逻辑被替换为 reducer 中的"直接变更"逻辑
-
不再需要为每种代码类型准备单独文件
-
我们建议将特定 reducer 的所有逻辑放在单个"slice"文件中
-
不再按"代码类型"分文件夹存放,而是按"功能"组织文件,将相关代码放在同一文件夹
-
理想情况下,reducers 和 actions 的命名应使用过去时态描述"已发生事件",而非命令式的"立即执行操作",例如使用
todoAdded而非ADD_TODO
这些存放常量、actions 和 reducers 的独立文件将被单个"slice"文件取代。现代化后的 slice 文件示例如下:
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 中的 "prepare" 标记来接收多个独立参数并创建 payload 字段。当 action 创建器需要执行额外操作(如为每个项生成唯一 ID)时,prepare 标记同样适用。
虽然 Redux Toolkit 不强制要求文件夹结构或 action 命名规则,但我们建议遵循这些最佳实践,经验证明它们能显著提升代码可维护性和可理解性。
使用 RTK Query 进行数据获取
传统的 React+Redux 应用数据获取通常需要多种组件和代码类型:
-
表示"请求开始"、"请求成功"和"请求失败"的 action 创建器和 action 类型
-
用于分发 actions 和执行异步请求的 thunks
-
跟踪加载状态和缓存数据的 reducers
-
从 store 读取这些值的 selectors
-
在组件挂载后通过类组件的
componentDidMount或函数组件的useEffect分发 thunk
这些逻辑通常分散在多个文件中:
export const FETCH_TODOS_STARTED = 'FETCH_TODOS_STARTED'
export const FETCH_TODOS_SUCCEEDED = 'FETCH_TODOS_SUCCEEDED'
export const FETCH_TODOS_FAILED = 'FETCH_TODOS_FAILED'
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))
}
}
}
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
}
}
export const selectTodosStatus = state => state.todos.status
export const selectTodos = state => state.todos.todos
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" action 类型触发 sagas,并使用 saga 文件替代 thunks:
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 消除了编写 actions、thunks、reducers、selectors 或 effects 来管理数据获取的需求(实际上它内部使用了这些工具)。此外,RTK Query 自动处理加载状态跟踪、请求去重和缓存数据生命周期管理(包括清理过期数据)。
迁移时,设置 RTK Query 的 "API slice" 定义并将生成的 reducer + 中间件加入 store:
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 => ({})
})
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 hooks:
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
最后在组件中使用这些 hooks:
import { useGetTodosQuery } from '../api/apiSlice'
export function TodoList() {
const { data: todos, isFetching, isSuccess } = useGetTodosQuery()
// omit rendering logic here
}
使用 createAsyncThunk 处理数据获取
我们特别推荐优先使用 RTK Query 处理数据获取。但若尚未准备好迁移,至少可使用 RTK 的 createAsyncThunk 减少手写 thunks 和 reducers 的样板代码。它会自动生成 action creators 和 action types,调用你提供的异步函数发起请求,并根据 promise 生命周期分发 actions。使用 createAsyncThunk 的相同示例如下所示:
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
你仍需手动编写 selectors,并在 useEffect 中分发 fetchTodos thunk。
使用 createListenerMiddleware 实现响应式逻辑
许多 Redux 应用包含"响应式"逻辑:监听特定 action 或状态变化,触发附加逻辑。这些功能通常通过 redux-saga 或 redux-observable 实现。
这些库用于处理多种任务。例如,监听 action 并延迟 1 秒分发新 action 的 saga 和 epic 可能如下:
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)
}
import { filter, mapTo } from 'rxjs/operators'
import { ofType } from 'redux-observable'
const pingEpic = action$ =>
action$.pipe(ofType('PING'), delay(1000), mapTo({ type: 'PONG' }))
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 的"监听器中间件"旨在替代 sagas 和 observables,提供更简洁的 API、更小的体积和更好的 TS 支持。
上述 saga 和 epic 示例可用监听器中间件替代:
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
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())
}
})
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 代码通常遵循非常冗长的类型定义模式:手动为每个 action 定义 TS 类型,并创建"action 类型联合"来限制 dispatch 可接收的具体 action。
我们明确且强烈反对这种模式!
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
})
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
}
}
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 的增强(如分发 thunks 的能力),并在修改 slice 状态或添加新 slice 时自动更新 RootState 类型。
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
每个切片文件都应声明并导出其自身切片状态的类型。随后,在 createSlice.reducers 内部使用 PayloadAction 类型声明所有 action 参数的类型。这样生成的 action 创建函数将自动获得正确的参数类型声明,并返回类型明确的 action.payload。
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 类组件转换为函数组件
-
在组件内部使用
useSelector和useDispatch钩子替代connect包装器
该过程可按组件逐个迁移。使用 connect 的组件与使用钩子的组件可同时共存。
本文不涵盖类组件转函数组件的具体过程,重点聚焦 React-Redux 特有的变更方案。
将 connect 迁移至钩子方案
使用 React-Redux connect API 的典型传统组件示例如下:
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传入的每个函数都变为组件内部定义的独立回调函数
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 类型:
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。
较优替代方案是 @types/react-redux v7.x 引入的 ConnectedProps<T> 类型,可自动推断 connect 传递给组件的所有 props 类型。但这需要将 connect 调用拆分为两部分以保证正确推断:
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 钩子 API 的 TypeScript 集成方案简洁得多! 钩子作为简单函数接收参数并返回结果,无需处理组件包装层、类型推断和泛型。仅需传递 RootState 和 AppDispatch 类型即可。
根据标准 TypeScript 设置与使用指南,我们推荐为钩子创建"预定义类型"别名,确保其内置正确类型,并在应用中统一使用这些预定义钩子。
首先配置钩子:
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>()
然后在组件中使用它们:
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
}
扩展阅读
更多细节请参阅以下文档页面和博客文章:
-
教程
-
附加文档
-
文章