- 來源:React Redux: Hooks
- 譯者:塔希
- 協議:CC BY-NC-SA 4.0
Hooks
React的新 "hooks" APIs 賦予了函式元件使用本地元件狀態,執行副作用,等各種能力。
React Redux 現在提供了一系列 hook APIs 作為現在 connect()
高階元件的替代品。這些 APIs 允許你,在不使用 connect()
包裹元件的情況下,訂閱 Redux 的 store,和 分發(dispatch) actions。
這些 hooks 首次新增於版本 v7.1.0。
在一個 React Redux 應用中使用 hooks
和使用 connect()
一樣,你首先應該將整個應用包裹在 <Provider>
中,使得 store 暴露在整個元件樹中。
const store = createStore(rootReducer)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
複製程式碼
然後,你就可以 import 下面列出的 React Redux hooks APIs,然後在函式元件中使用它們。
useSelector()
const result : any = useSelector(selector : Function, equalityFn? : Function)
複製程式碼
通過傳入 selector 函式,你就可以從從 Redux 的 store 中獲取 狀態(state) 資料。
警告: selector 函式應該是個純函式,因為,在任意的時間點,它可能會被執行很多次。
從概念上講,selector 函式與 connect
的 mapStateToProps
的引數是差不多一樣的。selector 函式被呼叫時,將會被傳入Redux store的整個state,作為唯一的引數。每次函式元件渲染時, selector 函式都會被呼叫。useSelector()
同樣會訂閱 Redux 的 sotre,並且在你每 分發(dispatch) 一個 action 時,都會被執行一次。
儘管如此,傳遞給 useSelector()
的各種 selector 函式還是和 mapState
函式有些不一樣的地方:
- selector 函式可以返回任意型別的值,並不要求是一個 物件(object)。selector 函式的返回值會被用作呼叫
useSelector()
hook 時的返回值。 - 當 分發(dispatch) 了一個 action 時,
useSelector()
會將上一次呼叫 selector 函式結果與當前呼叫的結果進行引用(===)比較,如果不一樣,元件會被強制重新渲染。如果一樣,就不會被重新渲染。 - selector 函式不會接收到
ownProps
引數。但是 props 可以通過閉包獲取使用(下面有個例子) 或者 通過使用柯里化的 selector 函式。 - 當使用 記憶後(memoizing) 的 selectors 函式時,需要一些額外的注意(下面有個例子幫助瞭解)。
useSelector()
預設使用嚴格比較===
來比較引用,而非淺比較。(看下面的部分來了解細節)
譯者注: 淺比較並不是指 ==。嚴格比較 === 對應的是 疏鬆比較 ==,與 淺比較 對應的是 深比較。
警告: 在 selectors 函式中使用 props 時存在一些邊界用例可能導致錯誤。詳見本頁的 使用警告 小節。
你可以在一個函式元件中多次呼叫 useSelector()
。每一個 useSelector()
的呼叫都會對 Redux 的 store 建立的一個獨立的 訂閱(subscription)。由於 Redux v7 的 批量更新(update batching) 行為,對於一個元件來說,如果一個 分發後(dispatched) 的 action 導致元件內部的多個 useSelector()
產生了新值,那麼僅僅會觸發一次重渲染。
相等比較(Equality Comparisons) 和更新
當一個函式元件渲染時,傳入的 selector 函式會被呼叫,其結果會作為 useSelector()
的返回值進行返回。(如果 selector 已經執行過,且沒有發生變化,可能會返回快取後的結果)
不管怎樣,當一個 action 被分發(dispatch) 到 Redux store 後,useSelector()
僅僅在 selector 函式執行的結果與上一次結果不同時,才會觸發重渲染。在版本v7.1.0-alpha.5中,預設的比較模式是嚴格引用比較 ===。這與 connect()
中的不同, connect()
使用淺比較來比較 mapState
執行後的結果,從而決定是否觸發重渲染。這裡有些建議關於如何使用useSelector()
。
對於 mapState
來講,所有獨立的狀態域被繫結到一個物件(object) 上返回。返回物件的引用是否是新的並不重要——因為 connect()
會單獨的比較每一個域。對於 useSelector()
來說,返回一個新的物件引用總是會觸發重渲染,作為 useSelector()
預設行為。如果你想獲得 store 中的多個值,你可以:
-
多次呼叫
useSelector()
,每次都返回一個單獨域的值 -
使用 Reselect 或類似的庫來建立一個記憶化的 selector 函式,從而在一個物件中返回多個值,但是僅僅在其中一個值改變時才返回的新的物件。
-
使用 React-Redux
shallowEqual
函式作為useSelector()
的equalityFn
引數,如:
import { shallowEqual, useSelector } from 'react-redux'
// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)
複製程式碼
這個可選的比較函式引數使得我們可以使用 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>
}
複製程式碼
使用記憶化的 selectors 函式
當像上方展示的那樣,在使用 useSelector
時使用單行箭頭函式,會導致在每次渲染期間都會建立一個新的 selector 函式。可以看出,這樣的 selector 函式並沒有維持任何的內部狀態。但是,記憶化的 selectors 函式 (通過 reselect
庫中 的 createSelector
建立) 含有內部狀態,所以在使用它們時必須小心。
當一個 selector 函式依賴於某個 狀態(state) 時,確保函式宣告在元件之外,這樣就不會導致相同的 selector 函式在每一次渲染時都被重複建立:
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumOfDoneTodos = createSelector(
state => state.todos,
todos => todos.filter(todo => todo.isDone).length
)
export const DoneTodosCounter = () => {
const NumOfDoneTodos = useSelector(selectNumOfDoneTodos)
return <div>{NumOfDoneTodos}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<DoneTodosCounter />
</>
)
}
複製程式碼
這種做法同樣適用於依賴元件 props 的情況,但是僅適用於單例的元件的形式
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumOfTodosWithIsDoneValue = createSelector(
state => state.todos,
(_, isDone) => isDone,
(todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
)
export const TodoCounterForIsDoneValue = ({ isDone }) => {
const NumOfTodosWithIsDoneValue = useSelector(state =>
selectNumOfTodosWithIsDoneValue(state, isDone)
)
return <div>{NumOfTodosWithIsDoneValue}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<TodoCounterForIsDoneValue isDone={true} />
</>
)
}
複製程式碼
如果, 你想要在多個元件例項中使用相同的依賴元件 props 的 selector 函式,你必須確保每一個元件例項建立屬於自己的 selector 函式(這裡解釋了為什麼這樣做是必要的)
import React, { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const makeNumOfTodosWithIsDoneSelector = () =>
createSelector(
state => state.todos,
(_, isDone) => isDone,
(todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
)
export const TodoCounterForIsDoneValue = ({ isDone }) => {
const selectNumOfTodosWithIsDone = useMemo(
makeNumOfTodosWithIsDoneSelector,
[]
)
const numOfTodosWithIsDoneValue = useSelector(state =>
selectNumOfTodosWithIsDone(state, isDone)
)
return <div>{numOfTodosWithIsDoneValue}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<TodoCounterForIsDoneValue isDone={true} />
<span>Number of unfinished todos:</span>
<TodoCounterForIsDoneValue isDone={false} />
</>
)
}
複製程式碼
被移除的:useActions()
useActions()
已經被移除
useDispatch()
const dispatch = useDispatch()
複製程式碼
這個 hook 返回 Redux store 的 分發(dispatch) 函式的引用。你也許會使用來 分發(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 函式將回撥函式記憶化,防止因為回撥函式引用的變化導致不必要的渲染。
譯者注:這裡的建議其實和 dispatch 沒關係,無論是否使用 dispatch,你都應該確保回撥函式不會無故變化,然後導致不必要的重渲染。之所以和 dispatch 沒關係,是因為,一旦 dispatch 變化,useCallback 會重新建立回撥函式,回撥函式的引用鐵定發生了變化,然而導致不必要的重渲染。
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>
))
複製程式碼
useStore()
const store = useStore()
複製程式碼
這個 hook 返回傳遞給 元件的 Redux sotore 的引用。
這個 hook 也許不應該被經常使用。 你應該將 useSelector()
作為你的首選。但是,在一些不常見的場景下,你需要訪問 store,這個還是有用的,比如替換 store 的 reducers。
例子
import React from 'react'
import { useStore } from 'react-redux'
export const CounterComponent = ({ value }) => {
const store = useStore()
// 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()}</div>
}
複製程式碼
自定義 context
<Provider>
元件允許你通過 context
引數指定一個可選的 context。在你構建複雜的可複用的元件時,你不想讓你自己的私人 store 與使用這個元件的使用者的 Redux store 發生衝突,這個功能是很有用的,
通過使用 hook creator 函式來建立自定義 hook,從而訪問可選的 context。
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 和 "喪屍子元件"
有關 React Redux 實現一個難點在於,當你以 (state, ownProps)
形式定義 mapStateToProps
函式時,怎麼保證每次都以最新的 props 呼叫 mapStateToProps
。version 4 中,在一些邊緣情況下,經常發生一些bug,比如一個列表中的某項被刪除時, mapState
函式內部會丟擲錯誤。
從 version 5 開始,React Redux 試圖保證 ownProps
引數的一致性。在 version 7 中,通過在 connect()
內部使用一個自定義的 Subscription
類,實現了這種保證,也導致了元件被層層巢狀的形式。這確保了元件樹深處 connect()
後的元件,只會在離自己最近的 connect()
後的祖先元件更新後,才會被通知 store 更新了。但是,這依賴於每個 connect(
) 的例項副高 React 內部部分的 context,隨後 connect()
提供了自己獨特的 Subscription
例項,將元件巢狀其中,提供一個新的 conext 值給 <ReactReduxContext.Provider>
,再進行渲染。
使用 hooks,意味著無法渲染 <ReactReduxContext.Provider>,也意味著沒有巢狀的訂閱層級。因此,“過期 Props” 和 "喪屍子元件" 的問題可能再次發生在你使用 hooks 而非 connect()
應用中。
詳細的說,“過期 Props”可能發生的狀況在於:
- 某個 selector 函式依賴元件的 props 來取回資料。
- 在某個 action 分發後,父元件將會重渲染然後傳遞新的props給子元件
- 但是子元件的 selector 函式在子元件以新props渲染前,先執行了。
取決於使用的 props 和 stroe 當前的 狀態(state) 是什麼,這可能導致返回不正確的資料,甚至丟擲一個錯誤。
"喪屍子元件" 特別指代下面這種情況:
-
在剛開始,多個巢狀
connect()
後的元件一起被掛載,導致子元件的訂閱先於其父元件。 -
一個 action 被 分發(dispatch) ,刪除了 store 中的某個資料,比如某個待做事項。
-
父元件會停止渲染對應的子元件
-
但是,因為子元件的訂閱先於父元件,其訂閱時的回撥函式的執行先於父元件停止渲染子元件。當子元件根據props取回對應的資料時,這個資料已經不存在了,而且,如果取回資料程式碼的邏輯不夠小心的話,可能會導致一個錯誤被丟擲。
useSelector()
通過捕獲所有 selector 內部因為 store 更新丟擲的錯誤(但不包括渲染時更新導致的錯誤),來應對"喪屍子元件"的問題。當產生了一個錯誤時,元件會被強制重渲染,此時,selector 函式會重新執行一次。注意,只有當你的 selector 函式是純函式且你的程式碼不依賴於 selector 丟擲的某些自定義錯誤時,這個應對策略才會正常工作。
如果你更想要自己處理這些問題,這裡有一些建議,在使用 useSelector()
時,可能幫助你避免這些問題。
-
在 selector 函式不要依賴 props 來取回資料。
-
對於你必須要依賴props,而且props經常改變的情況,以及,你取回的資料可能被刪除的情況下,試著帶有防禦性的 selector 函式。不要直接取回資料,如:
state.todos[props.id].name
- 先取回state.todos[props.id]
,然後檢驗值是否存在,再嘗試取回 todo.name -
因為 connect 增添了必要
Subscription
元件給 context provider,且延遲子元件訂閱的執行,一直到connect()
的元件重渲染後,在元件樹中,將一個connect()
的元件置於使用了useSelector
的元件之上,將會避免上述的問題,只要connect()
的元件和使用了 hooks 子元件觸發重渲染是由同一個 store 更新引起的。
注意:如果你想要這個問題更詳細的描述,這個聊天記錄詳述了這個問題,以及 issue #1179.
效能
正如上文提到的,在一個 action 被分發(dispatch) 後,useSelector()
預設對 select 函式的返回值進行引用比較 ===,並且僅在返回值改變時觸發重渲染。但是,不同於 connect(
),useSelector()
並不會阻止父元件重渲染導致的子元件重渲染的行為,即使元件的 props 沒有發生改變。
如果你想要類似的更進一步的優化,你也許需要考慮將你的函式元件包裹在 React.memo() 中:
const CounterComponent = ({ name }) => {
const counter = useSelector(state => state.counter)
return (
<div>
{name}: {counter}
</div>
)
}
export const MemoizedCounterComponent = React.memo(CounterComponent)
複製程式碼
Hooks 配方
我們精簡了原來 alpha 版本的 hooks API,專注於更精小的,更基礎的 API。不過,在你的應用中,你可能依舊想要使用一些我們以前實現過的方法。下面例子中的程式碼已經準備好被複制到你的程式碼庫中使用了。
配方:useActions()
這個 hook 存在於原來 alpha 版本,但是在版本 v7.1.0-alpha.4 中,Dan Abramov 的建議下被移除了。建議表明了在使用 hook 的場景下,“對 action creators 進行繫結”沒以前那麼有用,且會導致更多概念上理解負擔和增加語法上的複雜度。
譯者注:action creators 即用來生成 action 物件的函式。
在元件中,你應該更偏向於使用 useDispatch hook 來獲得 dispatch 函式的引用,然後在回撥函式中手動的呼叫 dispatch(someActionCreator()) 或某種需要的副作用。在你的程式碼中,你仍然可以使用bindActionCreators 函式繫結 action creators,或手動的繫結它們,比如 const boundAddTodo = (text) => dispatch(addTodo(text))。
但是,如果你自己想要使用這個 hook,這裡有個 複製即可用 的版本,支援將 action creators 作為一個獨立函式、陣列、或一個物件傳入。
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)
}
複製程式碼