[譯] React-Redux 官方 Hooks 文檔說明

Hooks

React的新 "hooks" APIs 賦予了函數組件使用本地組件狀態,執行反作用,等各類能力。html

React Redux 如今提供了一系列 hook APIs 做爲如今 connect() 高階組件的替代品。這些 APIs 容許你,在不使用 connect() 包裹組件的狀況下,訂閱 Redux 的 store,和 分發(dispatch) actions。react

這些 hooks 首次添加於版本 v7.1.0。git

在一個 React Redux 應用中使用 hooks

和使用 connect() 同樣,你首先應該將整個應用包裹在 <Provider> 中,使得 store 暴露在整個組件樹中。github

const store = createStore(rootReducer)

ReactDOM.render(
  <Provider store={store}> <App /> </Provider>,
  document.getElementById('root')
)
複製代碼

而後,你就能夠 import 下面列出的 React Redux hooks APIs,而後在函數組件中使用它們。redux

useSelector()

const result : any = useSelector(selector : Function, equalityFn? : Function)
複製代碼

經過傳入 selector 函數,你就能夠從從 Redux 的 store 中獲取 狀態(state) 數據。api

警告: selector 函數應該是個純函數,由於,在任意的時間點,它可能會被執行不少次。數組

從概念上講,selector 函數與 connectmapStateToProps 的參數是差很少同樣的。selector 函數被調用時,將會被傳入Redux store的整個state,做爲惟一的參數。每次函數組件渲染時, selector 函數都會被調用。useSelector()一樣會訂閱 Redux 的 sotre,而且在你每 分發(dispatch) 一個 action 時,都會被執行一次。緩存

儘管如此,傳遞給 useSelector() 的各類 selector 函數仍是和 mapState 函數有些不同的地方:bash

  • selector 函數能夠返回任意類型的值,並不要求是一個 對象(object)。selector 函數的返回值會被用做調用 useSelector() hook 時的返回值。
  • 當 分發(dispatch) 了一個 action 時,useSelector() 會將上一次調用 selector 函數結果與當前調用的結果進行引用(===)比較,若是不同,組件會被強制從新渲染。若是同樣,就不會被從新渲染。
  • selector 函數不會接收到 ownProps 參數。可是 props 能夠經過閉包獲取使用(下面有個例子) 或者 經過使用柯里化的 selector 函數。
  • 當使用 記憶後(memoizing) 的 selectors 函數時,須要一些額外的注意(下面有個例子幫助瞭解)。
  • useSelector() 默認使用嚴格比較 === 來比較引用,而非淺比較。(看下面的部分來了解細節)

譯者注: 淺比較並非指 ==。嚴格比較 === 對應的是 疏鬆比較 ==,與 淺比較 對應的是 深比較閉包

警告: 在 selectors 函數中使用 props 時存在一些邊界用例可能致使錯誤。詳見本頁的 使用警告 小節。

你能夠在一個函數組件中屢次調用 useSelector()。每個 useSelector() 的調用都會對 Redux 的 store 建立的一個獨立的 訂閱(subscription)。因爲 Redux v7 的 批量更新(update batching) 行爲,對於一個組件來講,若是一個 分發後(dispatched) 的 action 致使組件內部的多個 useSelector() 產生了新值,那麼僅僅會觸發一次重渲染。

相等比較(Equality Comparisons) 和更新

當一個函數組件渲染時,傳入的 selector 函數會被調用,其結果會做爲 useSelector() 的返回值進行返回。(若是 selector 已經執行過,且沒有發生變化,可能會返回緩存後的結果)

無論怎樣,當一個 action 被分發(dispatch) 到 Redux store 後,useSelector() 僅僅在 selector 函數執行的結果與上一次結果不一樣時,纔會觸發重渲染。在版本v7.1.0-alpha.5中,默認的比較模式是嚴格引用比較 ===。這與 connect() 中的不一樣, connect() 使用淺比較來比較 mapState 執行後的結果,從而決定是否觸發重渲染。這裏有些建議關於如何使用useSelector()

對於 mapState 來說,全部獨立的狀態域被綁定到一個對象(object) 上返回。返回對象的引用是不是新的並不重要——由於 connect() 會單獨的比較每個域。對於 useSelector() 來講,返回一個新的對象引用老是會觸發重渲染,做爲 useSelector() 默認行爲。若是你想得到 store 中的多個值,你能夠:

  • 屢次調用 useSelector(),每次都返回一個單獨域的值

  • 使用 Reselect 或相似的庫來建立一個記憶化的 selector 函數,從而在一個對象中返回多個值,可是僅僅在其中一個值改變時才返回的新的對象。

  • 使用 React-Redux shallowEqual 函數做爲 useSelector()equalityFn 參數,如:

import { shallowEqual, useSelector } from 'react-redux'

// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)
複製代碼

這個可選的比較函數參數使得咱們可使用 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>
}
複製代碼

經過閉包使用 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>
}
複製代碼

使用記憶化的 selectors 函數

當像上方展現的那樣,在使用 useSelector 時使用單行箭頭函數,會致使在每次渲染期間都會建立一個新的 selector 函數。能夠看出,這樣的 selector 函數並無維持任何的內部狀態。可是,記憶化的 selectors 函數 (經過 reselect 庫中 的 createSelector 建立) 含有內部狀態,因此在使用它們時必須當心。

當一個 selector 函數依賴於某個 狀態(state) 時,確保函數聲明在組件以外,這樣就不會致使相同的 selector 函數在每一次渲染時都被重複建立:

import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from '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 /> </> ) } 複製代碼

這種作法一樣適用於依賴組件 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} />
    </>
  )
}
複製代碼

若是, 你想要在多個組件實例中使用相同的依賴組件 props 的 selector 函數,你必須確保每個組件實例建立屬於本身的 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 =>
    selectNumOfTodosWithIsDone(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} />
    </>
  )
}
複製代碼

被移除的:useActions()

useActions() 已經被移除

useDispatch()

const dispatch = useDispatch()
複製代碼

這個 hook 返回 Redux store 的 分發(dispatch) 函數的引用。你也許會使用來 分發(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>
  )
}
複製代碼

在將一個使用了 dispatch 函數的回調函數傳遞給子組件時,建議使用 useCallback 函數將回調函數記憶化,防止由於回調函數引用的變化致使沒必要要的渲染。

譯者注:這裏的建議其實和 dispatch 不要緊,不管是否使用 dispatch,你都應該確保回調函數不會無端變化,而後致使沒必要要的重渲染。之因此和 dispatch 不要緊,是由於,一旦 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 sotore 的引用。

這個 hook 也許不該該被常用。 你應該將 useSelector() 做爲你的首選。可是,在一些不常見的場景下,你須要訪問 store,這個仍是有用的,好比替換 store 的 reducers。

例子

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>
}
複製代碼

自定義 context

<Provider> 組件容許你經過 context 參數指定一個可選的 context。在你構建複雜的可複用的組件時,你不想讓你本身的私人 store 與使用這個組件的用戶的 Redux store 發生衝突,這個功能是頗有用的,

經過使用 hook creator 函數來建立自定義 hook,從而訪問可選的 context。

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>
  )
}
複製代碼

使用警告

過時 Props 和 "喪屍子組件"

有關 React Redux 實現一個難點在於,當你以 (state, ownProps) 形式定義 mapStateToProps 函數時,怎麼保證每次都以最新的 props 調用 mapStateToProps。version 4 中,在一些邊緣狀況下,常常發生一些bug,好比一個列表中的某項被刪除時, mapState 函數內部會拋出錯誤。

從 version 5 開始,React Redux 試圖保證 ownProps 參數的一致性。在 version 7 中,經過在 connect() 內部使用一個自定義的 Subscription 類,實現了這種保證,也致使了組件被層層嵌套的形式。這確保了組件樹深處 connect() 後的組件,只會在離本身最近的 connect() 後的祖先組件更新後,纔會被通知 store 更新了。可是,這依賴於每一個 connect() 的實例副高 React 內部部分的 context,隨後 connect() 提供了本身獨特的 Subscription 實例,將組件嵌套其中,提供一個新的 conext 值給 <ReactReduxContext.Provider>,再進行渲染。

使用 hooks,意味着沒法渲染 <ReactReduxContext.Provider>,也意味着沒有嵌套的訂閱層級。所以,「過時 Props」 和 "喪屍子組件" 的問題可能再次發生在你使用 hooks 而非 connect() 應用中。

詳細的說,「過時 Props」可能發生的情況在於:

  • 某個 selector 函數依賴組件的 props 來取回數據。
  • 在某個 action 分發後,父組件將會重渲染而後傳遞新的props給子組件
  • 可是子組件的 selector 函數在子組件以新props渲染前,先執行了。

取決於使用的 props 和 stroe 當前的 狀態(state) 是什麼,這可能致使返回不正確的數據,甚至拋出一個錯誤。

"喪屍子組件" 特別指代下面這種狀況:

  • 在剛開始,多個嵌套 connect() 後的組件一塊兒被掛載,致使子組件的訂閱先於其父組件。

  • 一個 action 被 分發(dispatch) ,刪除了 store 中的某個數據,好比某個待作事項。

  • 父組件會中止渲染對應的子組件

  • 可是,由於子組件的訂閱先於父組件,其訂閱時的回調函數的運行先於父組件中止渲染子組件。當子組件根據props取回對應的數據時,這個數據已經不存在了,並且,若是取回數據代碼的邏輯不夠當心的話,可能會致使一個錯誤被拋出。

useSelector() 經過捕獲全部 selector 內部由於 store 更新拋出的錯誤(但不包括渲染時更新致使的錯誤),來應對"喪屍子組件"的問題。當產生了一個錯誤時,組件會被強制重渲染,此時,selector 函數會從新執行一次。注意,只有當你的 selector 函數是純函數且你的代碼不依賴於 selector 拋出的某些自定義錯誤時,這個應對策略纔會正常工做。

若是你更想要本身處理這些問題,這裏有一些建議,在使用 useSelector() 時,可能幫助你避免這些問題。

  • 在 selector 函數不要依賴 props 來取回數據。

  • 對於你必需要依賴props,並且props常常改變的狀況,以及,你取回的數據可能被刪除的狀況下,試着帶有防護性的 selector 函數。不要直接取回數據,如:state.todos[props.id].name - 先取回 state.todos[props.id],而後檢驗值是否存在,再嘗試取回 todo.name

  • 由於 connect 增添了必要 Subscription 組件給 context provider,且延遲子組件訂閱的執行,一直到 connect() 的組件重渲染後,在組件樹中,將一個 connect() 的組件置於使用了 useSelector 的組件之上,將會避免上述的問題,只要 connect() 的組件和使用了 hooks 子組件觸發重渲染是由同一個 store 更新引發的。

注意:若是你想要這個問題更詳細的描述,這個聊天記錄詳述了這個問題,以及 issue #1179.

性能

正如上文提到的,在一個 action 被分發(dispatch) 後,useSelector() 默認對 select 函數的返回值進行引用比較 ===,而且僅在返回值改變時觸發重渲染。可是,不一樣於 connect(),useSelector()並不會阻止父組件重渲染致使的子組件重渲染的行爲,即便組件的 props 沒有發生改變。

若是你想要相似的更進一步的優化,你也許須要考慮將你的函數組件包裹在 React.memo() 中:

const CounterComponent = ({ name }) => {
  const counter = useSelector(state => state.counter)
  return (
    <div> {name}: {counter} </div>
  )
}

export const MemoizedCounterComponent = React.memo(CounterComponent)
複製代碼

Hooks 配方

咱們精簡了原來 alpha 版本的 hooks API,專一於更精小的,更基礎的 API。不過,在你的應用中,你可能依舊想要使用一些咱們之前實現過的方法。下面例子中的代碼已經準備好被複制到你的代碼庫中使用了。

配方:useActions()

這個 hook 存在於原來 alpha 版本,可是在版本 v7.1.0-alpha.4 中,Dan Abramov 的建議下被移除了。建議代表了在使用 hook 的場景下,「對 action creators 進行綁定」沒之前那麼有用,且會致使更多概念上理解負擔和增長語法上的複雜度。

譯者注:action creators 即用來生成 action 對象的函數。

在組件中,你應該更偏向於使用 useDispatch hook 來得到 dispatch 函數的引用,而後在回調函數中手動的調用 dispatch(someActionCreator()) 或某種須要的反作用。在你的代碼中,你仍然可使用bindActionCreators 函數綁定 action creators,或手動的綁定它們,好比 const boundAddTodo = (text) => dispatch(addTodo(text))。

可是,若是你本身想要使用這個 hook,這裏有個 複製便可用 的版本,支持將 action creators 做爲一個獨立函數、數組、或一個對象傳入。

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)
}
複製代碼
相關文章
相關標籤/搜索