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,再到帶有掛勾函式的函式元件,Redux 也已從手動設定儲存體 + 手寫具物件擴充的簡約器 + React-Redux 的 connect,演進到 Redux Toolkit 的 configureStore + createSlice + React-Redux 的掛勾函式 API。

許多使用者處理的是舊版 Redux 程式碼庫,這些程式碼庫在「現代 Redux」模式出現之前就已存在。將這些程式碼庫遷移到現今建議的現代 Redux 模式,將會產生更小且更容易維護的程式碼庫。

好消息是,你可以逐步將程式碼遷移到現代 Redux,逐一進行,讓舊版和新版 Redux 程式碼並存且共同運作!

本頁涵蓋你可以用來現代化現有舊版 Redux 程式碼庫的常見方法和技術。

資訊

有關使用 Redux Toolkit + React-Redux 掛勾函式的「現代 Redux」如何簡化 Redux 使用方式的更多詳細資訊,請參閱下列其他資源

使用 Redux Toolkit 現代化 Redux 邏輯

遷移 Redux 邏輯的常見方法是

  • 將現有的手動 Redux 儲存體設定替換為 Redux Toolkit 的 configureStore
  • 選取現有的區塊簡約器及其相關動作。將這些替換為 RTK 的 createSlice。一次替換一個簡約器。
  • 視需要,將現有的資料擷取邏輯替換為 RTK Query 或 createAsyncThunk
  • 視需要,使用 RTK 的其他 API,例如 createListenerMiddlewarecreateEntityAdapter

你應該始終從將舊版 createStore 呼叫替換為 configureStore 開始。這是單次步驟,所有現有簡約器和中介軟體都將繼續照常運作。configureStore 包含開發模式檢查,以偵測常見錯誤,例如意外變異和不可序列化值,因此在程式碼庫中放置這些檢查有助於找出發生這些錯誤的任何區域。

資訊

您可以在 Redux 基本原理,第 8 部分:使用 Redux Toolkit 實作現代 Redux 中看到此一般方法的實際應用。

使用 configureStore 設定儲存

典型的舊版 Redux 儲存設定檔案會執行多個不同的步驟

  • 將區塊 reducer 合併至根 reducer
  • 建立中間件增強器,通常使用 thunk 中間件,以及開發模式中其他可能的 middleware,例如 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 方法,並自動為我們處理大部分的儲存設定。事實上,我們可以有效地將其簡化為一個步驟

基本儲存設定: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 儲存
  • 它自動加入 thunk 中間件並呼叫 applyMiddleware
  • 它自動加入更多中間件來檢查常見錯誤,例如意外變異狀態
  • 它自動設定 Redux DevTools 擴充功能連線

如果您的儲存設定需要額外的步驟,例如加入額外的中間件、傳遞 extra 參數給 thunk 中間件,或建立持久化的根 reducer,您也可以這麼做。以下是一個較大的範例,顯示如何自訂內建中間件並啟用 Redux-Persist,這展示了使用 configureStore 的一些選項

詳細範例:使用持久化和中間件的自訂儲存設定

此範例顯示設定 Redux 儲存時幾個可能的常見任務

  • 分別合併 reducer(有時由於其他架構限制而需要)
  • 加入額外的中間件,有條件和無條件
  • 傳遞「額外參數」給 thunk 中間件,例如 API 服務層
  • 使用 Redux-Persist 函式庫,它需要對其不可序列化的動作類型進行特殊處理
  • 在生產環境中關閉開發人員工具,並在開發環境中設定其他開發人員工具選項

這些都不是必要的,但它們確實經常出現在實際的程式碼庫中。

自訂儲存設定: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 的簡化器和動作

典型的舊版 Redux 程式碼庫將其簡化器邏輯、動作建立器和動作類型分散在不同的檔案中,而這些檔案通常按類型分組在不同的資料夾中。簡化器邏輯使用 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 旨在消除撰寫簡化器、動作和不可變更新的所有「樣板」!

使用 Redux Toolkit,對舊版程式碼有多項變更

  • createSlice 將完全消除手寫的動作建立器和動作類型
  • 所有唯一命名的欄位,例如 action.textaction.id,都將由 action.payload 取代,作為單獨的值或包含這些欄位的物件
  • 由於 Immer,手寫的不可變更新已由簡化器中的「變異」邏輯取代
  • 不需要為每種類型的程式碼建立不同的檔案
  • 我們教導在單一「切片」檔案中擁有給定簡化器的所有邏輯
  • 我們建議按「功能」整理檔案,而不是按「程式碼類型」建立不同的資料夾,讓相關程式碼放在同一個資料夾中
  • 理想情況下,簡化器和動作的命名應使用過去式,並描述「發生的事情」,而不是命令式的「現在執行這件事」,例如 todoAdded 而不是 ADD_TODO

這些用於常數、動作和簡化器的不同檔案都將由單一的「切片」檔案取代。現代化的切片檔案看起來像這樣

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 減速器中使用 「準備」符號 來接受多個獨立參數並建立 payload 欄位。準備 符號對於動作創建器執行其他工作(例如為每個項目產生唯一 ID)的情況也很有用。

儘管 Redux Toolkit 沒有特別關注您的資料夾和檔案結構或動作命名,但 這些是我們建議的最佳實務,因為我們發現它們有助於建立更易於維護和理解的程式碼。

使用 RTK Query 擷取資料

React+Redux 應用程式中典型的舊式資料擷取需要許多變動的部分和程式碼類型

  • 表示「請求開始」、「請求成功」和「請求失敗」動作的動作創建器和動作類型
  • Thunk 來發送動作並執行非同步請求
  • 追蹤載入狀態並儲存快取資料的減速器
  • 從儲存區讀取這些值的選取器
  • 在元件掛載後在元件中發送 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 函式庫來管理資料擷取,在這種情況下,他們可能會使用其他「訊號」動作類型來觸發 sagas,並使用此 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 或 effect 以管理資料擷取的需求。(事實上,它實際上使用所有這些相同的工具。)此外,RTK Query 會負責追蹤載入狀態、取消重複的請求,以及管理快取資料的生命週期(包括移除不再需要的過期資料)。

若要移轉,設定單一的 RTK Query「API 區段」定義,並將產生的 reducer + 中介軟體新增至你的儲存

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 的樣板。它會自動為你產生動作建立器和動作類型,呼叫你提供的非同步函式來提出請求,並根據承諾生命週期派送這些動作。使用 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-sagaredux-observable 函式庫實作。

這些函式庫用於各種任務。舉一個基本的範例,一個偵聽動作、等待一秒鐘,然後派送額外動作的 saga 和一個史詩可能如下所示

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」中介軟體旨在取代 saga 和 observable,具有更簡單的 API、更小的套件大小和更好的 TS 支援。

saga 和史詩範例可以用 listener 中介軟體取代,如下所示

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 Logic 遷移至 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 設定和使用指南,從設定儲存檔案開始,直接從儲存本身推論 AppDispatchRootState 類型。這將正確包含中介軟體新增至 dispatch 的任何修改,例如傳送 thunk 的能力,以及在修改區塊狀態定義或新增更多區塊時更新 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

每個區塊檔案都應該宣告並匯出其自己的區塊狀態類型。然後,使用 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
}

// 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 類別元件遷移為函式元件
  • connect 包覆器替換為在元件內部使用 useSelectoruseDispatch 勾子

您可以針對每個元件個別執行此操作。同時存在使用 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,然後計算元件所有 prop 的類型

簡單的 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> 類型,或手動計算封裝元件接收的所有 prop 的類型,並將它們作為泛型傳遞給 connect

一個稍微更好的替代方案是 ConnectedProps<T> 類型,它在 v7.x 中新增到 @types/react-redux,這使得能夠推斷 connect 會傳遞給元件的所有 prop 的類型。這確實需要將對 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 鉤子 API 與 TypeScript 一起使用簡單多了!與其處理元件封裝、類型推論和泛型的層級,鉤子是接受參數並回傳結果的簡單函式。你只需要傳遞 RootStateAppDispatch 的類型即可。

根據 我們的標準 TypeScript 設定和使用指南,我們特別教導設定鉤子的「預先輸入」別名,以便將正確的類型烘焙其中,並僅在應用程式中使用那些預先輸入的鉤子。

首先,設定鉤子

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
}

進一步資訊

查看這些文件頁面和部落格文章以取得更多詳細資訊