- 來源:React Redux: Hooks
- 譯者:塔希
- 協議:CC BY-NC-SA 4.0
React的新 "hooks" APIs 賦予了函數組件使用本地組件狀態,執行反作用,等各類能力。html
React Redux 如今提供了一系列 hook APIs 做爲如今 connect()
高階組件的替代品。這些 APIs 容許你,在不使用 connect()
包裹組件的狀況下,訂閱 Redux 的 store,和 分發(dispatch) actions。react
這些 hooks 首次添加於版本 v7.1.0。git
和使用 connect()
同樣,你首先應該將整個應用包裹在 <Provider>
中,使得 store 暴露在整個組件樹中。github
const store = createStore(rootReducer)
ReactDOM.render(
<Provider store={store}> <App /> </Provider>,
document.getElementById('root')
)
複製代碼
而後,你就能夠 import 下面列出的 React Redux hooks APIs,而後在函數組件中使用它們。redux
useSelector()
const result : any = useSelector(selector : Function, equalityFn? : Function)
複製代碼
經過傳入 selector 函數,你就能夠從從 Redux 的 store 中獲取 狀態(state) 數據。api
警告: selector 函數應該是個純函數,由於,在任意的時間點,它可能會被執行不少次。數組
從概念上講,selector 函數與 connect
的 mapStateToProps
的參數是差很少同樣的。selector 函數被調用時,將會被傳入Redux store的整個state,做爲惟一的參數。每次函數組件渲染時, selector 函數都會被調用。useSelector()
一樣會訂閱 Redux 的 sotre,而且在你每 分發(dispatch) 一個 action 時,都會被執行一次。緩存
儘管如此,傳遞給 useSelector()
的各類 selector 函數仍是和 mapState
函數有些不同的地方:bash
useSelector()
hook 時的返回值。useSelector()
會將上一次調用 selector 函數結果與當前調用的結果進行引用(===)比較,若是不同,組件會被強制從新渲染。若是同樣,就不會被從新渲染。ownProps
參數。可是 props 能夠經過閉包獲取使用(下面有個例子) 或者 經過使用柯里化的 selector 函數。useSelector()
默認使用嚴格比較 ===
來比較引用,而非淺比較。(看下面的部分來了解細節)譯者注: 淺比較並非指 ==。嚴格比較 === 對應的是 疏鬆比較 ==,與 淺比較 對應的是 深比較。閉包
警告: 在 selectors 函數中使用 props 時存在一些邊界用例可能致使錯誤。詳見本頁的 使用警告 小節。
你能夠在一個函數組件中屢次調用 useSelector()
。每個 useSelector()
的調用都會對 Redux 的 store 建立的一個獨立的 訂閱(subscription)。因爲 Redux v7 的 批量更新(update batching) 行爲,對於一個組件來講,若是一個 分發後(dispatched) 的 action 致使組件內部的多個 useSelector()
產生了新值,那麼僅僅會觸發一次重渲染。
當一個函數組件渲染時,傳入的 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 的比較功能。
基本用法:
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
時使用單行箭頭函數,會致使在每次渲染期間都會建立一個新的 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>
}
複製代碼
<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>
)
}
複製代碼
有關 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」可能發生的情況在於:
取決於使用的 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)
複製代碼
咱們精簡了原來 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)
}
複製代碼