React-redux 7.1發(fā)版啦。
因?yàn)樵谛碌捻?xiàng)目中用到了hooks,但是用的時(shí)候react-redux還處于alpha.x版本的狀態(tài)。用不了最新的API,感覺不是很美妙。好在,這兩天發(fā)布了7.1版本。
現(xiàn)在來看看怎么用這個(gè)新的API。
useSelector()
const result : any = useSelector(selector : Function, equalityFn? : Function)
這個(gè)是干啥的呢?就是從redux的store對象中提取數(shù)據(jù)(state)。
注意: 因?yàn)檫@個(gè)可能在任何時(shí)候執(zhí)行多次,所以你要保持這個(gè)selector是一個(gè)純函數(shù)。
這個(gè)selector方法類似于之前的connect的mapStateToProps參數(shù)的概念。并且useSelector會(huì)訂閱store, 當(dāng)action被dispatched的時(shí)候,會(huì)運(yùn)行selector。
當(dāng)然,僅僅是概念和mapStateToProps相似,但是肯定有不同的地方,看看selector和mapStateToProps的一些差異:
- selector會(huì)返回任何值作為結(jié)果,并不僅僅是對象了。然后這個(gè)selector返回的結(jié)果,就會(huì)作為
useSelector的返回結(jié)果。 - 當(dāng)action被dispatched的時(shí)候,
useSelector()將對前一個(gè)selector結(jié)果值和當(dāng)前結(jié)果值進(jìn)行淺比較。如果不同,那么就會(huì)被re-render。 反之亦然。 - selector不會(huì)接收ownProps參數(shù),但是,可以通過閉包(下面有示例)或使用柯里化selector來使用props。
- 使用記憶(memoizing) selector時(shí)必須格外小心(下面有示例)。
-
useSelector()默認(rèn)使用===(嚴(yán)格相等)進(jìn)行相等性檢查,而不是淺相等(==)。
你可能在一個(gè)組件內(nèi)調(diào)用useSelector多次,但是對useSelector()的每個(gè)調(diào)用都會(huì)創(chuàng)建redux store的單個(gè)訂閱。由于react-reduxv7版本使用的react的批量(batching)更新行為,造成同個(gè)組件中,多次useSelector返回的值只會(huì)re-render一次。
相等比較和更新
當(dāng)函數(shù)組件渲染時(shí),會(huì)調(diào)用提供的selector函數(shù),并且從useSelector返回其結(jié)果。(如果selector運(yùn)行且沒有更改,則會(huì)返回緩存的結(jié)果)。
上面有說到,只當(dāng)對比結(jié)果不同的時(shí)候會(huì)被re-render。從v7.1.0-alpha.5開始,默認(rèn)比較是嚴(yán)格比較(===)。這點(diǎn)于connect的時(shí)候不同,connect使用的是淺比較。這對如何使用useSelector()有幾個(gè)影響。
使用mapState,所有單個(gè)屬性都在組合對象中返回。返回的對象是否是新的引用并不重要 - connect()只比較各個(gè)字段。使用useSelector就不行了,默認(rèn)情況下是,如果每次返回一個(gè)新對象將始終進(jìn)行強(qiáng)制re-render。如果要從store中獲取多個(gè)值,那你可以這樣做:
useSelector()調(diào)用多次,每次返回一個(gè)字段值。使用Reselect或類似的庫創(chuàng)建一個(gè)記憶化(memoized) selector,它在一個(gè)對象中返回多個(gè)值,但只在其中一個(gè)值發(fā)生更改時(shí)才返回一個(gè)新對象。
使用react-redux 提供的
shallowEqual函數(shù)作為useSelector的equalityFn參數(shù)。
就像下面這樣:
import { shallowEqual, useSelector } from 'react-redux'
// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)
useSelector 例子
上面做了一些基本的闡述,下面該用一些例子來加深理解。
基本用法
import React from 'react'
import { useSelector } from 'react-redux'
export const CounterComponent = () => {
const counter = useSelector(state => state.counter)
return <div>{counter}</div>
}
通過閉包使用props來確定要提取的內(nèi)容:
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) selector
對于memoizing不是很了解的,可以通往此處了解。
當(dāng)使用如上所示的帶有內(nèi)聯(lián)selector的useSelector時(shí),如果渲染組件,則會(huì)創(chuàng)建selector的新實(shí)例。只要selector不維護(hù)任何狀態(tài),這就可以工作。但是,記憶化(memoizing) selectors 具有內(nèi)部狀態(tài),因此在使用它們時(shí)必須小心。
當(dāng)selector僅依賴于狀態(tài)時(shí),只需確保它在組件外部聲明,這樣一來,每個(gè)渲染所使用的都是相同的選擇器實(shí)例:
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect' //上面提到的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 />
</>
)
}
如果selector依賴于組件的props,但是只會(huì)在單個(gè)組件的單個(gè)實(shí)例中使用,則情況也是如此:
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} />
</>
)
}
但是,如果selector被用于多個(gè)組件實(shí)例并且依賴組件的props,那么你需要確保每個(gè)組件實(shí)例都有自己的selector實(shí)例(為什么要這樣?看這里):
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 =>
selectNumOfTodosWithIsDoneValue(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} />
</>
)
}
useDispatch()
const dispatch = useDispatch()
這個(gè)Hook返回Redux store中對dispatch函數(shù)的引用。你可以根據(jù)需要使用它。
用法和之前的一樣,來看個(gè)例子:
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í),建議使用useCallback對其進(jìn)行記憶,否則子組件可能由于引用的更改進(jìn)行不必要地呈現(xiàn)。
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()
這個(gè)Hook返回redux <Provider>組件的store對象的引用。
這個(gè)鉤子應(yīng)該不長被使用。useSelector應(yīng)該作為你的首選。但是,有時(shí)候也很有用。來看個(gè)例子:
import React from 'react'
import { useStore } from 'react-redux'
export const CounterComponent = ({ value }) => {
const store = useStore()
// 僅僅是個(gè)例子! 不要在你的應(yīng)用中這樣做.
// 如果store中的state改變,這個(gè)將不會(huì)自動(dòng)更新
return <div>{store.getState()}</div>
}
使用的警告
舊的props和"Zombie Children"
React Redux實(shí)現(xiàn)中最困難的一個(gè)方面是,如果mapStateToProps函數(shù)被定義為(state, ownProps),那么如何確保每次都會(huì)使用“最新”的props調(diào)用它。
在版本7中,它是在connect()內(nèi)部使用自定義的Subscription類實(shí)現(xiàn)的,它構(gòu)成了一個(gè)嵌套層次結(jié)構(gòu)。這可以確保樹中較低的組件只有在更新了最接近的祖先后才會(huì)收到store更新通知。
對于Hooks版,無法渲染上下文提供者,這意味著也沒有嵌套的訂閱層次結(jié)構(gòu)。因此,“陳舊的props”問題可能會(huì)在依賴于使用Hooks而不是connect()的應(yīng)用程序中重新出現(xiàn)。
具體來說,陳舊的props指:
- selector函數(shù)依賴于組件的props去提取數(shù)據(jù)
- 父組件將重新渲染并把操作的結(jié)果作為新props傳遞
- 但是此組件的selector函數(shù)在此組件有機(jī)會(huì)使用這些新props重新渲染之前執(zhí)行
根據(jù)使用的props和當(dāng)前存儲狀態(tài),這可能導(dǎo)致從選擇器返回不正確的數(shù)據(jù),甚至引發(fā)錯(cuò)誤。
"Zombie child"(僵尸兒童?棄子?)特指以下情況:
- 在第一次傳遞中mounted多個(gè)嵌套的connected的組件,導(dǎo)致子組件在其父級之前訂閱該存儲
- 被調(diào)度(dispatch)的操作從存儲中刪除數(shù)據(jù)
- 因此,父組件將停止呈現(xiàn)該子組件
- 但是,子級首先被訂閱,他的訂閱在父級停止渲染他之前運(yùn)行。當(dāng)它基于props從store中讀取值時(shí),該數(shù)據(jù)不再存在,并且如果提取邏輯不謹(jǐn)慎,則可能導(dǎo)致拋出錯(cuò)誤。
useSelector()試圖通過捕獲由于store更新而執(zhí)行selector時(shí)拋出的所有錯(cuò)誤來處理這個(gè)問題(但不是在渲染期間執(zhí)行selector時(shí))。發(fā)生錯(cuò)誤時(shí),將強(qiáng)制組件呈現(xiàn),此時(shí)將再次執(zhí)行selector。只要選擇器是純函數(shù),并且不依賴于選擇器拋出錯(cuò)誤,這就可以工作。
如果你想自己處理這個(gè)問題,可以使用useSelector():
- 對于提取數(shù)據(jù)的selector函數(shù),不要依賴props
- 如果selector函數(shù)確實(shí)依賴props,而這些props可能隨著時(shí)間的推移而變化,或者提取的數(shù)據(jù)可能是可刪除的項(xiàng),那么請嘗試防御性地編寫selector函數(shù)。不要直接
state.todos [props.id] .name- 首先讀取state.todos[props.id],讀取todo.name之前驗(yàn)證它是否存在。 - 因?yàn)?code>connect添加了必要的
Subscription到上下文的provider和延遲執(zhí)行子級訂閱,直到connected的組件re-render,使用useSelector將一個(gè)連接的組件放在組件樹中組件的正上方,只要connected的組件由于與hook組件相同的store更新而re-render,就可以防止這些問題。
性能
前面說了,selector的值改變會(huì)造成re-render。但是這個(gè)與connect有些不同,useSelector()不會(huì)阻止組件由于其父級re-render而re-render,即使組件的props沒有更改。
如果需要進(jìn)一步的性能優(yōu)化,可以在React.memo()中包裝函數(shù)組件:
const CounterComponent = ({ name }) => {
const counter = useSelector(state => state.counter)
return (
<div>
{name}: {counter}
</div>
)
}
export const MemoizedCounterComponent = React.memo(CounterComponent)
Hooks 配方
配方: useActions()
這個(gè)是alpha的一個(gè)hook,但是在alpha.4中聽取Dan的建議被移除了。這個(gè)建議是基于“binding actions creator”在基于鉤子的用例中沒啥特別的用處,并且導(dǎo)致了太多的概念開銷和語法復(fù)雜性。
你可能更喜歡直接使用useDispatch。你可能也會(huì)使用Redux的bindActionCreators函數(shù)或者手動(dòng)綁定他們,就像這樣: const boundAddTodo = (text) => dispatch(addTodo(text))。
但是,如果你仍然想自己使用這個(gè)鉤子,這里有一個(gè)現(xiàn)成的版本,它支持將action creator作為單個(gè)函數(shù)、數(shù)組或?qū)ο髠鬟f進(jìn)來。
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] : deps)
}
配方: useShallowEqualSelector()
import { shallowEqual } from 'react-redux'
export function useShallowEqualSelector(selector) {
return useSelector(selector, shallowEqual)
}
代碼注解:branch-v7.1