鉤子
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()
預設使用嚴格的 ===
參考相等檢查,而非淺層相等(更多資訊請見以下章節)。
選擇器在概念上與connect
的 mapStateToProps
參數大致相同。
你可以在一個單一的函數組件中多次呼叫 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
中使用內嵌選取器時,如上所示,每當元件被呈現時,就會建立選取器的一個新執行個體。只要選取器不維護任何狀態,這便可行。然而,記憶化選取器(例如透過 reselect
的 createSelector
建立)確實有內部狀態,因此在使用時必須小心。以下您可以找到記憶化選取器的典型使用情境。
當選擇器只依賴狀態時,只需確定它宣告於元件外部,以便於每個渲染都使用相同的選擇器實例
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
變數應新增至 useEffect
和 useCallback
的相依性陣列中。最簡單的解決方案就是這麼做
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 和取捨 中很好地總結了這些問題。