跳到主要內容

鉤子

React 的 「鉤子」API 讓函式元件可以使用本機元件狀態、執行副作用等功能。React 也讓我們可以撰寫 自訂鉤子,讓我們可以擷取可重複使用的鉤子,以在 React 內建鉤子之上加入我們的自訂行為。

React Redux 包含它自己的自訂鉤子 API,這讓你的 React 元件可以訂閱 Redux 商店並發送動作。

提示

我們建議在你的 React 元件中以 React-Redux 鉤子 API 作為預設方法。

現有的 connect API 仍可運作,而且將持續提供支援,但鉤子 API 較為簡潔,而且與 TypeScript 的相容性較佳。

這些鉤子最早在 v7.1.0 中加入。

在一個 React Redux APP 中使用 Hooks

就像 connect() 一樣,你一開始會將你的整個應用程式用 <Provider> 組件包起來,讓儲存庫可以透過組件樹取得

const store = createStore(rootReducer)

// As of React 18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>,
)

從那裡開始,你就可以導入任何列出的 React Redux 函數 API,並在你的函數組件中使用它們。

useSelector()

type RootState = ReturnType<typeof store.getState>
type SelectorFn = <Selected>(state: RootState) => Selected
type EqualityFn = (a: any, b: any) => boolean
export type DevModeCheckFrequency = 'never' | 'once' | 'always'

interface UseSelectorOptions {
equalityFn?: EqualityFn
devModeChecks?: {
stabilityCheck?: DevModeCheckFrequency
identityFunctionCheck?: DevModeCheckFrequency
}
}

const result: Selected = useSelector(
selector: SelectorFn,
options?: EqualityFn | UseSelectorOptions
)

讓你從 Redux 儲存庫狀態中,透過使用選擇器函數,萃取出資料來使用在這個組件中。

資訊

選擇器函數應該是純函數,因為它有可能在任意時間點被執行多次。

請參閱 Redux 文件中的使用 Redux:透過選擇器推導資料,以取得更多關於撰寫和使用選擇器函數的相關資訊。

選擇器會只以整個 Redux 儲存庫狀態當作它的唯一參數被呼叫。選擇器可以回傳任何值作為結果,包括直接回傳一個儲存在 state 中的巢狀值,或推導出新的值。選擇器的回傳值會被用作 useSelector() 函數的回傳值。

只要函數組件會重新渲染時,選擇器就會執行(除非它的參考自組件上次渲染後沒有改變,且函數可以透過不重新執行選擇器而回傳快取的結果)。useSelector() 也會訂閱 Redux 儲存庫,並在你調度一個動作時執行你的選擇器。

當一個動作被調度時,useSelector() 會對前一個選擇器結果值和目前的結果值進行參考比較。若它們不同,組件將會被強制重新渲染。若它們相同,組件則不會重新渲染。useSelector() 預設使用嚴格的 === 參考相等檢查,而非淺層相等(更多資訊請見以下章節)。

選擇器在概念上與connectmapStateToProps 參數大致相同。

你可以在一個單一的函數組件中多次呼叫 useSelector()。每一對 useSelector() 的呼叫都會對 Redux 儲存庫建立單獨的訂閱。由於 React Redux v7 中使用了 React 的更新批次處理行為,一個調度動作導致同一個組件中多個 useSelector() 回傳新值應該只會產生一 次重新渲染。

資訊

在選取器中使用 props 可能會導致問題,存在潛在邊緣個案。請參閱本頁的 使用警告 說明以了解進一步詳細資料。

等號比較和更新

當函式元件呈現時,提供的選取器函式將會被呼叫,其結果將從 useSelector() 鉤子返回。(鉤子可能會在不重新執行選取器的情況下傳回快取的結果,如果它是與元件先前呈現相同的函式參考。

但是,當一個動作被分派到 Redux 儲存區時,useSelector() 只有在選取器結果看似異於上一個結果時,才會強制重新呈現。預設的比較是一個嚴格的 === 參考比較。這與 connect() 不同,後者使用 mapState 呼叫的結果進行淺層等號檢查,以判斷是否需要重新呈現。這對你該如何使用 useSelector() 有幾個影響。

mapState 中,所有個別欄位都在一個組合物件中傳回。無論傳回物件是否是一個新的參考,connect() 都只是比較個別欄位。在 useSelector() 中,每次傳回一個新的物件預設都會強制重新呈現。如果您想從儲存區中擷取多個值,您可以

  • 多次呼叫 useSelector(),每次呼叫傳回一個單一欄位值
  • 使用 Reselect 或類似的函式庫建立一個記憶化選取器,在單一物件中傳回多個值,但僅在其中一個值變更時才傳回一個新物件。
  • 將來自 React-Redux 的 shallowEqual 函式用於 useSelector()equalityFn 參數,例如
import { shallowEqual, useSelector } from 'react-redux'

// Pass it as the second argument directly
const selectedData = useSelector(selectorReturningObject, shallowEqual)

// or pass it as the `equalityFn` field in the options argument
const selectedData = useSelector(selectorReturningObject, {
equalityFn: shallowEqual,
})
  • 將自訂等號函式用於 useSelector()equalityFn 參數,例如
import { useSelector } from 'react-redux'

// equality function
const customEqual = (oldValue, newValue) => oldValue === newValue

// later
const selectedData = useSelector(selectorReturningObject, customEqual)

這個可選的比較函式也使您能使用類似 Lodash 的 _.isEqual() 或 Immutable.js 的比較功能。

useSelector 範例

基本使用方式

import React from 'react'
import { useSelector } from 'react-redux'

export const CounterComponent = () => {
const counter = useSelector((state) => state.counter)
return <div>{counter}</div>
}

透過閉包使用 props 來判斷要擷取什麼

import React from 'react'
import { useSelector } from 'react-redux'

export const TodoListItem = (props) => {
const todo = useSelector((state) => state.todos[props.id])
return <div>{todo.text}</div>
}

使用記憶化選取器

useSelector 中使用內嵌選取器時,如上所示,每當元件被呈現時,就會建立選取器的一個新執行個體。只要選取器不維護任何狀態,這便可行。然而,記憶化選取器(例如透過 reselectcreateSelector 建立)確實有內部狀態,因此在使用時必須小心。以下您可以找到記憶化選取器的典型使用情境。

當選擇器只依賴狀態時,只需確定它宣告於元件外部,以便於每個渲染都使用相同的選擇器實例

import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'

const selectNumCompletedTodos = createSelector(
(state) => state.todos,
(todos) => todos.filter((todo) => todo.completed).length,
)

export const CompletedTodosCounter = () => {
const numCompletedTodos = useSelector(selectNumCompletedTodos)
return <div>{numCompletedTodos}</div>
}

export const App = () => {
return (
<>
<span>Number of completed todos:</span>
<CompletedTodosCounter />
</>
)
}

如果選擇器依賴於元件的 props,但只在單一元件的單一實例中使用時,也是如此

import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'

const selectCompletedTodosCount = createSelector(
(state) => state.todos,
(_, completed) => completed,
(todos, completed) =>
todos.filter((todo) => todo.completed === completed).length,
)

export const CompletedTodosCount = ({ completed }) => {
const matchingCount = useSelector((state) =>
selectCompletedTodosCount(state, completed),
)

return <div>{matchingCount}</div>
}

export const App = () => {
return (
<>
<span>Number of done todos:</span>
<CompletedTodosCount completed={true} />
</>
)
}

不過,當選擇器用於多個元件實例且依賴於元件的 props 時,您需要確定選擇器的記憶化行為已正確設定(詳情請參閱這裡)。

開發模式檢查

useSelector 在開發模式下會執行一些額外的檢查,以偵測意外的行為。在生產版本中,不會執行這些檢查。

資訊

這些檢查首先新增於 v8.1.0 版

選擇器結果穩定性

在開發環境中,提供給選擇器的函式會在第一次呼叫 useSelector 時再執行一次,使用相同的參數;如果選擇器傳回不同的結果(基於所提供的 equalityFn),將會在主控台中發出警告。

這一點很重要,因為如果選擇器在使用相同的輸入再次呼叫時傳回不同的結果參考,將會導致不必要的重新渲染

// this selector will return a new object reference whenever called,
// which causes the component to rerender after *every* action is dispatched
const { count, user } = useSelector((state) => ({
count: state.count,
user: state.user,
}))

如果選擇器結果適當地穩定(或選擇器已被記憶化),它不會傳回不同的結果,也不會記錄任何警告。

預設情況下,這只會在初次呼叫選擇器時發生。您可以在 Provider 中或每次呼叫 useSelector 時設定檢查。

通過脈絡進行的全球設定
<Provider store={store} stabilityCheck="always">
{children}
</Provider>
個別勾子的設定
function Component() {
const count = useSelector(selectCount, {
devModeChecks: { stabilityCheck: 'never' },
})
// run once (default)
const user = useSelector(selectUser, {
devModeChecks: { stabilityCheck: 'once' },
})
// ...
}

單位式函式 (state => state) 檢查

重大變更!

先前這稱為 noopCheck

在開發環境中,會對選擇器傳回的結果進行檢查。如果結果與傳入的參數(即根狀態)相同,則會在主控台中發出警告。

useSelector 呼叫傳回整個根狀態幾乎總是錯誤的,因為這表示當狀態中的任何內容 變更時,元件將重新渲染。選擇器應盡可能細化,例如 state => state.some.nested.field

// BAD: this selector returns the entire state, meaning that the component will rerender unnecessarily
const { count, user } = useSelector((state) => state)

// GOOD: instead, select only the state you need, calling useSelector as many times as needed
const count = useSelector((state) => state.count.value)
const user = useSelector((state) => state.auth.currentUser)

預設情況下,這只會在初次呼叫選擇器時發生。您可以在 Provider 中或每次呼叫 useSelector 時設定檢查。

通過脈絡進行的全球設定
<Provider store={store} identityFunctionCheck="always">
{children}
</Provider>
個別勾子的設定
function Component() {
const count = useSelector(selectCount, {
devModeChecks: { identityFunctionCheck: 'never' },
})
// run once (default)
const user = useSelector(selectUser, {
devModeChecks: { identityFunctionCheck: 'once' },
})
// ...
}

相較於 connect

傳遞給 useSelector()mapState 函式的選取器有一些差異

  • 選取器可以傳回任何值作為結果,而不僅僅是物件。
  • 選取器通常應該只傳回一個值,而不是物件。如果你傳回一個物件或陣列,請務必使用快取選取器,以避免不必要的重新渲染。
  • 選取器函式不會接收 ownProps 參數。但是,道具可以使用閉包(請參閱上述範例)或使用花式選取器來使用。
  • 您可以使用 `equalityFn` 選項自訂比對行為

useDispatch()

import type { Dispatch } from 'redux'
const dispatch: Dispatch = useDispatch()

此鉤子會傳回 Redux 儲存中的 dispatch 函式的參考。您可以使用它依照需要發送 action。

範例

import React from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()

return (
<div>
<span>{value}</span>
<button onClick={() => dispatch({ type: 'increment-counter' })}>
Increment counter
</button>
</div>
)
}

當使用 dispatch 傳遞回呼到子元件時,您可能會有時想要使用 useCallback 將其快取。如果 子元件使用 React.memo() 或類似方法嘗試最佳化渲染行為,這樣可以避免由於回呼參考值變更而導致子元件不必要地重新渲染。

import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
const incrementCounter = useCallback(
() => dispatch({ type: 'increment-counter' }),
[dispatch],
)

return (
<div>
<span>{value}</span>
<MyIncrementButton onIncrement={incrementCounter} />
</div>
)
}

export const MyIncrementButton = React.memo(({ onIncrement }) => (
<button onClick={onIncrement}>Increment counter</button>
))
資訊

只要將同一個儲存體實例傳遞給 <Provider>dispatch 函式的參考將保持穩定。通常,在一個應用程式中該儲存體實例永遠不變。

但是,React 鉤子檢查規則不知道 dispatch 應該保持穩定,並會警告 dispatch 變數應新增至 useEffectuseCallback 的相依性陣列中。最簡單的解決方案就是這麼做

export const Todos = () => {
const dispatch = useDispatch()

useEffect(() => {
dispatch(fetchTodos())
// Safe to add dispatch to the dependencies array
}, [dispatch])
}

useStore()

import type { Store } from 'redux'
const store: Store = useStore()

此鉤子會傳回與傳遞給 <Provider> 元件的 Redux 儲存體相同的參考。

可能不常使用此鉤子。優先將 useSelector() 作為您的主要選擇。但是,這可能對不常見的腳本有所幫助,而這些腳本確實需要存取儲存體,例如替換 reducer。

範例

import React from 'react'
import { useStore } from 'react-redux'

export const ExampleComponent = ({ value }) => {
const store = useStore()

const onClick = () => {
// Not _recommended_, but safe
// This avoids subscribing to the state via `useSelector`
// Prefer moving this logic into a thunk instead
const numTodos = store.getState().todos.length
}

// EXAMPLE ONLY! Do not do this in a real app.
// The component will not automatically update if the store state changes
return <div>{store.getState().todos.length}</div>
}

自訂背景

<Provider> 元件允許您透過 context 道具指定替換背景。如果您正在建置複雜的可重複使用元件,並且不希望您的儲存體與消費者應用程式可能使用的任何 Redux 儲存體發生衝突,這會很有用。

若要透過鉤子 API 存取替換背景,請使用鉤子建立函式

import React from 'react'
import {
Provider,
createStoreHook,
createDispatchHook,
createSelectorHook,
} from 'react-redux'

const MyContext = React.createContext(null)

// Export your custom hooks if you wish to use them in other files.
export const useStore = createStoreHook(MyContext)
export const useDispatch = createDispatchHook(MyContext)
export const useSelector = createSelectorHook(MyContext)

const myStore = createStore(rootReducer)

export function MyProvider({ children }) {
return (
<Provider context={MyContext} store={myStore}>
{children}
</Provider>
)
}

使用警告

過時的 Props 和「殭屍子元件」

資訊

自從我們於 v7.1.0 中釋出 React-Redux hooks API 以後,它就已經準備好可以投入生產了,而且**我們建議將 hooks API 用作元件中的預設方法**。不過,有一些邊緣狀況可能會發生,而且**我們記錄下來這些狀況,這樣你便會 aware**。

實際上,這些情況很少見 - 我們收到的關於這些情況出現在文件中的評論,遠多於關於這些情況實際上成為應用程式問題的報告。

React Redux 實作最困難的層面之一,就是確保如果將 `mapStateToProps` 函式定義為 `(state, ownProps)`,那麼它會每次都使用「最新的」props 來呼叫。直到版本 4 為止,都會出現重複報告的 bug,其中涉及邊緣狀況,例如針對列表項目的 `mapState` 函式拋出錯誤,而該列表項目的資料才剛被刪除。

從版本 5 開始,React Redux 便嘗試確保與 `ownProps` 的一致性。在版本 7 中,這是使用 `connect()` 中內部的自訂 `Subscription` 類別來實作,它會形成一個巢狀階層。這會確保樹狀結構中較低層的連線元件,只會在最近的連線祖先更新之後,才會收到儲存更新通知。不過,這仰賴每個 `connect()` 實體覆寫 React 內部 context 的一部分,提供它自己的 unique `Subscription` 實體以形成該巢狀,並使用新的 context 值呈現 `<ReactReduxContext.Provider>`。

使用 hooks 之後,就沒辦法呈現 context 提供者,這表示也沒有 subscription 的巢狀階層。因此,仰賴使用 hooks 而不是 `connect()` 的應用程式,可能會再度發生「過時的 props」和「殭屍子元件」問題。

具體來說,「過時的 props」是指任何一種

  • 選取器函式仰賴此元件的 props 才能萃取資料
  • 父元件**會**重新呈現並傳遞新的 props 作為動作的結果
  • 但此元件的選取器函式在元件有機會用這些新的 props 重新呈現之前執行

這**可能會**導致從選取器傳回不正確的資料,甚至導致拋出錯誤,具體視使用的 props 和目前的儲存狀態而定。

「殭屍子元件」特別指的是

  • 多個巢狀的連線元件會在第一次傳遞中裝載,導致子元件在父元件之前訂閱儲存
  • 已派送的動作,從儲存中刪除資料,例如代辦事項
  • 結果是,父元件停止呈現子元件
  • 然而,由於子元件先前已訂閱,其訂閱在父元件停止呈現前執行。當它根據 props 從儲存中讀取值時,資料已不存在,如果萃取邏輯未仔細檢查,這可能會導致發生錯誤。

useSelector() 嘗試透過捕捉儲存更新時執行選取器導致的所有錯誤來處理此問題(但不會在呈現期間執行時進行捕捉)。當錯誤發生時,元件將強制呈現,選取器此時會再次執行。只要選取器是純函數,且您不依賴選取器拋出錯誤,這就會正常運作。

如果您偏好自行處理此問題,以下列出一些可能的選項可透過 useSelector() 完全避免這些問題

  • 請勿仰賴選取器函數中的 props 來萃取資料
  • 在仰賴您的選取器函數中的 props 這些 props 可能會隨時間改變的情況下,您萃取的資料可能是基於可刪除的項目時,請嘗試保護性地撰寫選取器函數。不要直接使用 state.todos[props.id].name 來讀取-請先讀取 state.todos[props.id],並驗證它是否存在,再嘗試讀取 todo.name
  • 由於 connect 將必要的 Subscription 新增到內容提供者中,並延後評估子元件訂閱,直到已連線的元件重新呈現為止,只要已連線的元件會因為與 hooks 元件相同的儲存更新而重新呈現,就能防止出現這些問題。

效能

如先前所述,在預設情況下,useSelector() 在動作被派送後執行選取器函數時,會比較選取值的參考等效性,並且只會在選取值改變時,造成元件重新呈現。然而,與 connect() 不同的是,即使元件的 props 沒有改變,useSelector() 仍會因其父元件重新呈現而導致元件重新呈現。

如果需要進一步的效能最佳化,您可以考慮將您的函數元件包裝在 React.memo()

const CounterComponent = ({ name }) => {
const counter = useSelector((state) => state.counter)
return (
<div>
{name}: {counter}
</div>
)
}

export const MemoizedCounterComponent = React.memo(CounterComponent)

Hooks 指南

我們已精簡我們的掛勾 API,從最初 Alpha 發布版,專注於更精簡的一組 API 原語。不過,你可能仍然希望在自己的應用程式中,使用我們在你的程式中嘗試過的一些方法。這些範例應已準備好複製及貼到你的程式碼中。

配方:useActions()

此掛勾已存在我們的最初 Alpha 發布版中,但已在 v7.1.0-alpha.4 中移除,根據 Dan Abramov 的建議。該建議是基於「繫結動作建立器」在基於掛勾的用例中並不實用,而且會造成過多的概念負擔和語法複雜性。

建議你更喜歡在你的元件中呼叫 useDispatch 掛勾以擷取 dispatch 的參照,並在需要時手動在回呼和效果中呼叫 dispatch(someActionCreator())。你也可以在自己的程式碼中使用 Redux bindActionCreators 函數來繫結動作建立器,或「手動」繫結它們,就像 const boundAddTodo = (text) => dispatch(addTodo(text))

不過,如果你仍然想要自己使用此掛勾,以下是一個可複製貼上的版本,它支援將動作建立器傳入作為一個函數、一個陣列或一個物件。

import { bindActionCreators } from 'redux'
import { useDispatch } from 'react-redux'
import { useMemo } from 'react'

export function useActions(actions, deps) {
const dispatch = useDispatch()
return useMemo(
() => {
if (Array.isArray(actions)) {
return actions.map((a) => bindActionCreators(a, dispatch))
}
return bindActionCreators(actions, dispatch)
},
deps ? [dispatch, ...deps] : [dispatch],
)
}

配方:useShallowEqualSelector()

import { useSelector, shallowEqual } from 'react-redux'

export function useShallowEqualSelector(selector) {
return useSelector(selector, shallowEqual)
}

在使用掛勾時的額外考量

在決定是否使用掛勾時,有一些架構的取捨需要考慮。Mark Erikson 在他的兩篇網誌文章 關於 React Hooks、Redux 和分工Hooks、HOC 和取捨 中很好地總結了這些問題。