原文:https://react-redux.js.org/api/hooks
翻譯水平有待提高,虛心接受各位看官的指教,歡迎大家留言自己的見(jiàn)解
目錄
1.在React Redux應(yīng)用中使用Hooks
2.useSelector()
3.useDispatch()
4.useStore()
5.自定義 context
6.注意事項(xiàng)
7.Hooks 示例
簡(jiǎn)介
React的新特性 Hooks讓函數(shù)組件可以使用類(lèi)似Class組件的State等執(zhí)行副作用。React讓我們還可以自定Hooks,自定義的Hooks可以在React自帶的Hooks之上抽離可以復(fù)用的操作。
React Redux 集成了自己定義的Hooks,這些Hooks可以讓你的React組件訂閱Redux store 和發(fā)送action
我們推薦使用React-Redux Hooks 作為React組件的默認(rèn)實(shí)現(xiàn)方式。
已經(jīng)存在的connect函數(shù)仍然可以使用并且會(huì)繼續(xù)提供支持,但是Hooks更加簡(jiǎn)單,與TypeScript使用效果會(huì)更好。
這些Hooks函數(shù)支持的最低版本 :7.1.0。
1.在React Redux應(yīng)用中使用Hooks#
使用 connect()
,開(kāi)始你需要把整個(gè)應(yīng)用包裹在 <Provider>
組件中,確保store可以被整個(gè)組件樹(shù)訪(fǎng)問(wèn)到:
const store = createStore(rootReducer)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
在這之后,你就可以引用所有的React Redux Hooks 并可以在函數(shù)組件中使用它們。
2.useSelector()#
const result: any = useSelector(selector: Function, equalityFn?: Function)
通過(guò)selector可以讓你從Redux store state中獲取到數(shù)據(jù)。
INFO
selector應(yīng)該是純函數(shù),因?yàn)樗赡軙?huì)在任何時(shí)間點(diǎn)被執(zhí)行多次。
selector概念上基本相當(dāng)于 connect
里的參數(shù) mapStateToProps
。當(dāng)函數(shù)組件渲染時(shí)selector就會(huì)執(zhí)行(除非selector本身相較于之前組件渲染時(shí)沒(méi)有變化,這時(shí)候Hook就會(huì)返回緩存的結(jié)果而不是重新運(yùn)行selector)。 useSelector()
也會(huì)訂閱 Redux store,當(dāng)action執(zhí)行后會(huì)執(zhí)行selector。
然而,selector跟 useSelector()
和mapState
還是有很多不同:
- selector可以把任何值作為結(jié)果返回,不只是object。selector的返回值會(huì)被作為
useSelector()
的返回值。 - 當(dāng)一個(gè)action執(zhí)行后,
useSelector()
會(huì)把之前selector的值與當(dāng)前的值做比較。 - selector 函數(shù)不會(huì)接收
ownProps
參數(shù)。然而,props可以通過(guò)閉包使用(看下面的例子),或者通過(guò)柯里化的selector使用。 - 當(dāng)使用memoizing selector時(shí)需要特別注意(看下面的例子)。
-
useSelector()
默認(rèn)使用嚴(yán)格的===
做比較,不是通過(guò)淺比較(看下面的例子)。
INFO
有些在selector里使用props導(dǎo)致問(wèn)題的情況,參照本文注意事項(xiàng)章節(jié)獲取更多細(xì)節(jié)。
在一個(gè)函數(shù)組件中,你可能會(huì)調(diào)用多次 useSelector()
。每次調(diào)用useSelector()
都會(huì)對(duì)Redux store創(chuàng)建一個(gè)獨(dú)立的訂閱。因?yàn)镽eact Redux v7版本中React 的更新是批處理的,在同一個(gè)組件中一個(gè)action的分發(fā)本來(lái)應(yīng)該只會(huì)導(dǎo)致一次重新渲染,但卻會(huì)引起多個(gè)useSelector()
返回新的值。
比較與更新#
當(dāng)一個(gè)函數(shù)組件渲染時(shí),提供的selector函數(shù)會(huì)執(zhí)行,結(jié)果也會(huì)通過(guò)useSelector()
返回(如果是組件中的同一個(gè)函數(shù)的實(shí)例,則不會(huì)重新執(zhí)行selector,而是通過(guò)hook返回之前緩存的結(jié)果)。
當(dāng)一個(gè)aciton分發(fā)到Redux store后,useSelector()
只會(huì)在selector結(jié)果與之前結(jié)果不同時(shí)才會(huì)強(qiáng)制重新渲染。v7.1.0-alpha.5版本,默認(rèn)的比較方式是嚴(yán)格的 ===
強(qiáng)比較。這個(gè)跟connect()
不同,connect()
只是淺比較了mapState
的結(jié)果來(lái)決定是否需要重新渲染。這可能會(huì)影響你如何使用useSelector()
。
使用mapState
,所有的變量都會(huì)通過(guò)一個(gè)組合的object返回。這并不關(guān)心返回的object是否是新的值——connect()
只比較object中的每個(gè)變量。
使用 useSelector()
,返回一個(gè)新的object默認(rèn)肯定會(huì)重新渲染。如果你想從store中得到多個(gè)值,你可以:
多次調(diào)用
useSelector()
,每次返回一個(gè)變量的值。使用Reselect或者是類(lèi)似的庫(kù),創(chuàng)建一個(gè)memoizing的selector,可以通過(guò)一個(gè)object返回多個(gè)值。僅在一個(gè)值改變后就返回一個(gè)新的object。
使用React-Redux 的淺比較函數(shù)作為
useSelector()
的參數(shù)equalityFn
。useSelector()
使用React-Redux中的淺比函數(shù)作為比較函數(shù),比如:
import { shallowEqual, useSelector } from 'react-redux'
// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)
這個(gè)可配置的比較函數(shù)也可以使用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>
}
通過(guò)閉包使用props取決于需要獲取什么數(shù)據(jù):
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>
}
使用 memoizing selectors#
如上面展示的,在內(nèi)聯(lián) selector中使用 useSelector
時(shí),當(dāng)組件渲染完后會(huì)創(chuàng)建一個(gè)新的selector實(shí)例。這個(gè)實(shí)例會(huì) 一直有效直到不再持有任何state。 當(dāng)然,memoizing selector (比如 在 reselect
中通過(guò) createSelector
創(chuàng)建) 確實(shí)會(huì)有一個(gè)內(nèi)部的state,使用它們時(shí)必須注意。下面你會(huì)看到一些 memoizing selector的典型應(yīng)用場(chǎng)景。
當(dāng)selector只依賴(lài)于state時(shí),僅需要確保它是在組件外聲明的,這樣同一個(gè)selector就可以被每次渲染使用:
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 />
</>
)
}
如果selector依賴(lài)于組件的props,同一個(gè)是確定的。但是,只會(huì)被一個(gè)組件的一個(gè)實(shí)例使用:
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} />
</>
)
}
但是,當(dāng)selector被多個(gè)組件實(shí)例使用并且依賴(lài)組件的props時(shí),你需要確定每一個(gè)組件的實(shí)例都能得到他自己的那個(gè)selector實(shí)例參見(jiàn)這里 ,獲取進(jìn)一步解釋?zhuān)瑸槭裁催@是必須的:
import React, { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const makeSelectCompletedTodosCount = () =>
createSelector(
(state) => state.todos,
(_, completed) => completed,
(todos, completed) =>
todos.filter((todo) => todo.completed === completed).length
)
export const CompletedTodosCount = ({ completed }) => {
const selectCompletedTodosCount = useMemo(makeSelectCompletedTodosCount, [])
const matchingCount = useSelector((state) =>
selectCompletedTodosCount(state, completed)
)
return <div>{matchingCount}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<CompletedTodosCount completed={true} />
<span>Number of unfinished todos:</span>
<CompletedTodosCount completed={false} />
</>
)
}
3.useDispatch()#
const dispatch = useDispatch()
useDispatch()
向 dispatch
返回了一個(gè)Redux store實(shí)例。你可以用它來(lái)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>
)
}
當(dāng)使用 dispatch
向子組件傳遞回調(diào)時(shí),有時(shí)候你可能想通過(guò) useCallback
來(lái)memoize它。子組件可以通過(guò)React.memo()
等來(lái)優(yōu)化渲染操作,這樣可以避免子組件因回調(diào)函數(shù)(參數(shù)是個(gè)函數(shù))改變導(dǎo)致的不必要渲染。
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>
))
INFO
只要通過(guò)<Provider>
傳遞同一個(gè)store實(shí)例,dispatch
函數(shù)就是穩(wěn)定的。一般一個(gè)應(yīng)用中store實(shí)例不會(huì)改變。
當(dāng)然, React hook的檢測(cè)規(guī)則不會(huì)知道dispatch
是否應(yīng)該穩(wěn)定,并且會(huì)警告dispatch
變量應(yīng)該添加到useEffect
和useCallback
的依賴(lài)數(shù)組中。最簡(jiǎn)單的解決辦法如下:
export const Todos() = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchTodos())
// Safe to add dispatch to the dependencies array
}, [dispatch])
}
4.useStore()#
const store = useStore()
useStore返回了與傳遞給 <Provider>
一樣的Redux store實(shí)例。
useStore不建議頻繁使用。建議把useSelector()
作為首選方式。
當(dāng)然,這個(gè)可能對(duì)個(gè)別需要使用store場(chǎng)景比較有用,比如代替 reducer。
示例#
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>
}
5.自定義 context#
<Provider>
組件可以讓你通過(guò)context
屬性,指定一個(gè)自定義的context。如果你創(chuàng)建一個(gè)可復(fù)用的復(fù)雜組件這個(gè)設(shè)置會(huì)很有用,可以避免自定義應(yīng)用內(nèi)的Redux store沖突。
通過(guò)Hooks使用可替換的 context,可以使用hook創(chuàng)建函數(shù):
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>
)
}
6.注意事項(xiàng)#
無(wú)用Props 和僵尸子節(jié)點(diǎn)#
INFO
React-Redux Hooks 從v7.1.0版本開(kāi)始可以穩(wěn)定使用了。我們推薦使用Hooks作為組件的默認(rèn)實(shí)現(xiàn)方式。但是也有些邊界用例問(wèn)題出現(xiàn),我們記錄了這些問(wèn)題以便大家能知道這些問(wèn)題。
React Redux在實(shí)現(xiàn)的時(shí)候最復(fù)雜的一個(gè)操作是需要保證mapStateToProps
函數(shù)結(jié)構(gòu)類(lèi)似 (state, ownProps)
這樣,每次props更新后都會(huì)調(diào)用。從版本4.0開(kāi)始就有反復(fù)出現(xiàn)了一些bug,比如:mapState
函數(shù)因列表的item數(shù)據(jù)被刪除了而拋出異常。
從5.0版本開(kāi)始,React Redux試圖通過(guò)ownProps
來(lái)確保一致性。在7.0版本,在connect()
內(nèi)部通過(guò)自定義的Subscription
來(lái)實(shí)現(xiàn),形成了嵌套調(diào)用。這樣確保了在組件樹(shù)子節(jié)點(diǎn)的組件只會(huì)在最近的鏈接的父節(jié)點(diǎn)更新后才會(huì)收到store更新的通知。但是這依賴(lài)于每個(gè)connect()
實(shí)例重寫(xiě)部分內(nèi)部React context,用新的context支持自己獨(dú)有的Subscription
來(lái)組建這個(gè)嵌套調(diào)用,使用心得context作為 <ReactReduxContext.Provider>
的參數(shù)。
使用Hooks,不會(huì)渲染context provider,也就是不會(huì)有嵌套層級(jí)的訂閱。基于此,應(yīng)用如果使用Hook代替connect()
可能會(huì)復(fù)現(xiàn)無(wú)用props和僵尸子節(jié)點(diǎn)問(wèn)題。
確切的說(shuō),無(wú)用props意味著:
- selector函數(shù)依賴(lài)這個(gè)組件的props提取數(shù)據(jù)。
- 父組件會(huì)重新渲染并且會(huì)以props的形式傳遞action的結(jié)果。
- 組件的selector函數(shù),在組建接受到新的props重新渲染之前就執(zhí)行了。
Depending on what props were used and what the current store state is, this may result in incorrect data being returned from the selector, or even an error being thrown.
依賴(lài)于調(diào)用了什么樣的props跟什么樣的store state,這會(huì)導(dǎo)致selector返回不正確的數(shù)據(jù),甚至拋出異常。
僵尸子組件確切的說(shuō)是指如下情況:
- 首次加載多個(gè)嵌套的關(guān)聯(lián)的組建會(huì)導(dǎo)致子組件在父組件之前訂閱store信息。
- action會(huì)從store中返回刪除的數(shù)據(jù),比如todo item。
- 父組件會(huì)停止渲染子組件。
- 因?yàn)樽咏M件先訂閱了store信息,子組件的訂閱會(huì)在父組件停止渲染它之前運(yùn)行。當(dāng)讀取到store基于props的值時(shí),如果數(shù)據(jù)不存在了,并且獲取邏輯不嚴(yán)謹(jǐn),可能會(huì)導(dǎo)致拋出異常。
useSelector()
就是用來(lái)解決以上問(wèn)題的:通過(guò)捕獲所有由于store更新(不是在渲染期間執(zhí)行)而導(dǎo)致selector運(yùn)行產(chǎn)生的異常。只要selector是一個(gè)純函數(shù)并且不依賴(lài)于selector拋出異常這個(gè)就會(huì)有效。
如果你傾向于自己處理這個(gè)問(wèn)題,下面是些可行的使用useSelector()
時(shí)避免上述問(wèn)題的方案:
- selector函數(shù)不要依賴(lài)props獲取數(shù)據(jù)。
- 以防你的selector函數(shù)確實(shí)需要依賴(lài)于props,這些props可能會(huì)改變,或者你獲取的數(shù)據(jù)可能依賴(lài)于會(huì)不會(huì)刪除的item,selector函數(shù)要定義的保守一點(diǎn)。不要只是直接用
state.todos[props.id].name
,先獲取state.todos[props.id]
,確保值存在再獲取todo.name
。 - 因?yàn)?code>connect在context provider中添加了必要的
Subscription
并且延時(shí)執(zhí)行子組件的訂閱操作直到關(guān)聯(lián)的組建重新渲染完。在關(guān)聯(lián)組件因同一個(gè)store更新而重新渲染時(shí),組件使用useSelector
之前把關(guān)聯(lián)的組建放入組件樹(shù)可以避免上述問(wèn)題。
INFO
關(guān)于此場(chǎng)景更多的描述如下:
- "Stale props and zombie children in Redux" by Kai Hao
- this chat log that describes the problems in more detail
- issue #1179
性能#
如早先提到的,使用默認(rèn)的useSelector()
當(dāng)action執(zhí)行后運(yùn)行selector函數(shù)會(huì)對(duì)選中的值進(jìn)行比較,只有當(dāng)選中的值改變時(shí)組件才會(huì)重新渲染。但是不像connect()
,useSelector()
會(huì)因?yàn)楦附M件的重新渲染而而重新渲染子組件,即使子組件里的props沒(méi)有改變。
如果性能優(yōu)化是必須的,你需要考慮把你的函數(shù)組件包裹到 React.memo()
中:
const CounterComponent = ({ name }) => {
const counter = useSelector(state => state.counter)
return (
<div>
{name}: {counter}
</div>
)
}
export const MemoizedCounterComponent = React.memo(CounterComponent)
7.Hooks 示例#
基于初始的alpha release,我們對(duì)Hooks的API進(jìn)行了精簡(jiǎn)。更關(guān)注較小的API原語(yǔ)。但是可能在你的應(yīng)用中你仍然想使用一些我們?cè)囘^(guò)的方式。下面的示例代碼可以直接復(fù)制并粘貼到你自己的代碼中。
方法: useActions()
#
useActions()
在之前的release分支中,但是基于Dan Abramov 的建議](https://github.com/reduxjs/react-redux/issues/1252#issuecomment-488160930)在v7.1.0-alpha.4
版本移除掉了。這個(gè)建議是基于"binding action creators"在基于hook的使用場(chǎng)景下沒(méi)有用,并且會(huì)導(dǎo)致很多概念上的成本與句法的復(fù)雜性。
你可能更傾向于調(diào)用 useDispatch
在你的組件中獲取dispatch
的實(shí)例。然后根據(jù)需要在回調(diào)或者effect中手動(dòng)調(diào)用dispatch(someActionCreator())
。你的代碼中還可以使用Redux bindActionCreators
函數(shù)來(lái)綁定action creator,或者像const boundAddTodo = (text) => dispatch(addTodo(text))
一樣綁定它們。
如果你確實(shí)喜歡使用Hooks,下面是可以復(fù)制粘貼的版本,支持通過(guò)action creator傳遞函數(shù)、數(shù)組或者
對(duì)象。
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)
}
使用Hooks的額外注意事項(xiàng)#
當(dāng)考慮是否使用Hooks時(shí)有些架構(gòu)方面的問(wèn)題需要權(quán)衡。Mark Erikson在他發(fā)表的兩篇博客中總結(jié)的很好。 Thoughts on React Hooks, Redux, and Separation of Concerns 和 Hooks, HOCs, and Tradeoffs。