react-redux@7.1的api,用于hooks

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函數作為useSelectorequalityFn參數。

就像下面這樣:

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)
}

原文:apiv7.1-hooks.md

代碼注解:branch-v7.1

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容