react-redux@7.1的api,用于hooks

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ù)作為useSelectorequalityFn參數(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)
}

原文:apiv7.1-hooks.md

代碼注解:branch-v7.1

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容