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