[Перевод] Официальная документация по хукам React-Redux Описание

внешний интерфейс

Hooks

Реакт новый"hooks" APIsДает функциональным компонентам возможность использовать состояние локального компонента, выполнять побочные эффекты и многое другое.

React Redux теперь предоставляет набор API-интерфейсов хуков, какconnect()Альтернатива компонентам более высокого порядка. Эти API позволяют вам, не используяconnect()В случае обернутых компонентов подпишитесь на хранилище Redux и отправьте действия.

Эти хуки были впервые добавлены в версии v7.1.0.

Использование хуков в приложении React Redux

и использоватьconnect()Например, вы должны сначала обернуть все приложение в<Provider>, чтобы хранилище отображалось по всему дереву компонентов.

const store = createStore(rootReducer)

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

Затем вы можете импортировать API-интерфейсы перехватчиков React Redux, перечисленные ниже, и использовать их в функциональных компонентах.

useSelector()

const result : any = useSelector(selector : Function, equalityFn? : Function)

Передав функцию селектора, вы можете получить данные о состоянии из хранилища Redux.

предупреждать: функция выбора должна бытьчистая функция, потому что в любой момент времени он может выполняться много раз.

Концептуально функция селектора такая же, какconnectизmapStateToPropsпараметрыпочти то же самое. При вызове функции селектора ей будет передано все состояние хранилища Redux в качестве единственного параметра. Функция селектора вызывается каждый раз при рендеринге функционального компонента.useSelector()Он также подписывается на сотре Redux и будет выполняться каждый раз, когда вы отправляете действие.

Тем не менее переходите кuseSelector()Различные функции селектора неподвижны иmapStateФункции немного отличаются:

  • Функция выбора может возвращать значение любого типа и не обязательно должна быть объектом. Возвращаемое значение функции селектора будет использоваться как вызовuseSelector()Возвращаемое значение при подключении.
  • Когда действие отправлено,useSelector()Сравнит результат последнего вызова функции-селектора с результатом текущего вызова (===), если нет, то компонент будет принудительно перерендерен. Если это то же самое, он не будет повторно отображаться.
  • функция селектора не получитownPropsпараметр. Но доступ к свойствам можно получить через замыкания (пример ниже) или с помощью функции каррированного селектора.
  • При использовании запоминающих селекторов требуется особая осторожность (пример ниже поможет понять).
  • useSelector()Использовать строгое сравнение по умолчанию===сравнивать ссылки, а не поверхностные сравнения. (подробности см. в разделе ниже)

Примечание переводчика: поверхностное сравнение не означает ==. Строгое сравнение === соответствует свободному сравнению ==, аналогичномуповерхностное сравнениесоответствуетглубокое сравнение.

предупреждать: есть некоторые крайние случаи, которые могут привести к ошибкам при использовании реквизитов в функциях селекторов. Подробнее см. на этой страницеиспользовать предупреждениеподраздел.

Вы можете вызывать несколько раз в функциональном компонентеuseSelector(). КаждыйuseSelector()Каждый вызов создает отдельную подписку на хранилище Redux. Из-за поведения пакетного обновления Redux v7 для компонента, если отправленное действие вызывает несколькоuseSelector()Генерируется новое значение, затем запускается только один повторный рендеринг.

Сравнения и обновления равенства

Когда функциональный компонент визуализируется, будет вызвана переданная функция селектора, и результат будет использоваться какuseSelector()Возвращаемое значение возвращается. (Если селектор уже был выполнен и не изменился, может быть возвращен кешированный результат)

Однако, когда действие отправляется в хранилище Redux,useSelector()Повторный рендеринг будет запущен только в том случае, если результат выполнения функции селектора отличается от предыдущего результата. В версии v7.1.0-alpha.5 режим сравнения по умолчанию — строгое сравнение ссылок ===. Это то же самое, чтоconnect()разница вconnect()Используйте неглубокие сравнения для сравненияmapStateРезультат после выполнения, чтобы решить, запускать ли повторную визуализацию. Вот несколько советов о том, как использоватьuseSelector().

дляmapStateДругими словами, все отдельные поля состояния привязываются к объекту и возвращаются. Неважно, является ли ссылка на возвращаемый объект новой, потому чтоconnect()Каждое поле сравнивается отдельно. дляuseSelector()Например, возврат ссылки на новый объект всегда вызывает повторную визуализацию, т.к.useSelector()Поведение по умолчанию. Если вы хотите получить несколько значений в магазине, вы можете:

  • несколько вызововuseSelector(), каждый раз возвращая значение отдельного поля

  • Используйте Reselect или аналогичную библиотеку, чтобы создать функцию мемоизированного селектора, которая возвращает несколько значений в объекте, но возвращает новый объект только при изменении одного из значений.

  • Использование React-ReduxshallowEqualфункционировать какuseSelector()изequalityFnпараметры, такие как:

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

// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)

Этот необязательный параметр функции сравнения позволяет нам использовать Lodash_.isEqual()Или функция сравнения Immutable.js.

пример использования селектора

Основное использование:

import React from 'react'
import { useSelector } from 'react-redux'

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

Замыкание использует свойства, чтобы выбрать, какое состояние получить:

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

Используйте функции запоминаемых селекторов

При использованииuseSelectorПри использовании функции однострочной стрелки это приведет к созданию новой функции выбора во время каждого рендеринга. Можно видеть, что такая функция-селектор не поддерживает никакого внутреннего состояния. Однако функция запомненных селекторов (черезreselectв библиотекеcreateSelectorcreate) содержат внутреннее состояние, поэтому вы должны быть осторожны при их использовании.

Когда функция-селектор зависит от состояния, убедитесь, что функция объявлена ​​вне компонента, чтобы одна и та же функция-селектор не создавалась повторно при каждом рендеринге:

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

Этот подход также применим к случаю зависимых компонентных реквизитов, но только в виде одноэлементного компонента.

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

Если вы хотите использовать одну и ту же функцию выбора для нескольких экземпляров компонента в зависимости от свойств компонента, вы должны убедиться, что каждый экземпляр компонента создает свою собственную функцию выбора (здесьобъясняет зачем это нужно)

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

Этот хук возвращает ссылку на функцию отправки хранилища Redux. Вы можете использовать это для отправки некоторых необходимых действий.

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

При передаче функции обратного вызова, которая использует функцию отправки, в дочерний компонент, рекомендуется использовать функцию useCallback для запоминания функции обратного вызова, чтобы предотвратить ненужный рендеринг из-за изменений в ссылке на функцию обратного вызова.

Примечание переводчика: Совет здесь не имеет ничего общего с диспетчеризацией.Используете ли вы диспетчеризацию или нет, вы должны убедиться, что функция обратного вызова не изменяется без причины и не вызывает ненужного повторного рендеринга. Причина, по которой это не имеет никакого отношения к диспетчеризации, заключается в том, что после изменения диспетчеризации 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()

Этот хук возвращает ссылку на сотор Redux, переданный компоненту.

Этот хук, вероятно, не следует использовать очень часто. ты должен положитьuseSelector()как ваш первый выбор. Однако в некоторых необычных сценариях, когда вам нужно получить доступ к хранилищу, это все же полезно, например, при замене редукторов хранилища.

пример

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

пользовательский контекст

<Provider> компоненты позволяют пройтиcontextПараметр указывает необязательный контекст. Это полезно, когда вы создаете сложные повторно используемые компоненты, когда вы не хотите, чтобы ваше собственное хранилище конфликтовало с хранилищем Redux пользователя, использующего компонент.

Получите доступ к дополнительному контексту, используя функции создания хуков для создания пользовательских хуков.

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

использовать предупреждение

Устаревшие реквизиты и «зомби-подкомпонент»

Одна из трудностей с реализациями React Redux заключается в том, что когда вы начинаете с(state, ownProps)формальное определениеmapStateToPropsКогда функция используется, как убедиться, что она вызывается с последними реквизитами каждый разmapStateToProps. В версии 4 в некоторых крайних случаях часто возникают некоторые ошибки, например, когда элемент в списке удаляется,mapStateВнутри функции возникает ошибка.

Начиная с версии 5, React Redux пытается гарантироватьownPropsСогласованность параметров. В версии 7 черезconnect()Внутренне используйте пользовательскийSubscriptionКлассы, реализующие эту гарантию, также приводят к форме, в которой компоненты вложены слоями. Это гарантирует, что глубоко внутри дерева компонентовconnect()После компонента он будет только в ближайшемconnect()После обновления компонента-предка он будет уведомлен об обновлении хранилища. Впрочем, это зависит от каждого connect() экземпляр, подчиненный контексту внутреннего раздела React, за которым следуетconnect()обеспечивает свою уникальнуюSubscriptionэкземпляр, в который вложен компонент, предоставляя новое значение контекста для<ReactReduxContext.Provider>, а затем визуализировать.

Использование хуков означает, что не может быть отрисован, что означает отсутствие вложенных иерархий подписки. Таким образом, проблемы с «истекшими свойствами» и «зомби-субкомпонентами» могут возникнуть снова, если вы используете хуки вместоconnect()в приложении.

Подробно возможные ситуации «истекшего Props» следующие:

  • Функция селектора использует свойства компонента для получения данных.
  • После отправки действия родительский компонент будет повторно отображать и передавать новые реквизиты дочернему компоненту.
  • Но функция селектора дочернего компонента выполняется до того, как дочерний компонент будет отображаться с новыми реквизитами.

В зависимости от используемых реквизитов и текущего состояния stroe это может привести к возврату неверных данных или даже к ошибке.

«Подкомпонент зомби» конкретно относится к следующим случаям:

  • В начале несколько вложенныхconnect()Последующие компоненты монтируются вместе, в результате чего дочерний компонент подписывается раньше своего родителя.

  • Отправляется действие, которое удаляет некоторые данные в хранилище, например элемент списка дел.

  • Родительский компонент прекратит рендеринг соответствующего дочернего компонента.

  • Однако, поскольку подписка дочернего компонента предшествует родительскому компоненту, функция обратного вызова для его подписки запускается до того, как родительский компонент прекратит визуализацию дочернего компонента. Когда дочерний компонент извлекает соответствующие данные в соответствии с реквизитами, данные больше не существуют, и, если логика кода извлечения недостаточно осторожна, это может привести к возникновению ошибки.

useSelector()Решает проблему «дочернего компонента-зомби», перехватывая все ошибки, возникающие внутри селектора из-за обновлений хранилища (но исключая ошибки, вызванные обновлениями во время рендеринга). При возникновении ошибки компонент будет принудительно перерендерен, и функция селектора будет выполнена повторно. Обратите внимание, что эта стратегия преодоления будет работать только в том случае, если ваша функция селектора чистая, и ваш код не зависит от какой-либо пользовательской ошибки, выдаваемой селектором.

Если вы предпочитаете решать эти проблемы самостоятельно, вот несколько советов по использованиюuseSelector()может помочь вам избежать этих проблем.

  • Не полагайтесь на реквизиты для получения данных в функциях выбора.

  • В случаях, когда вам приходится полагаться на реквизиты, которые часто меняются, и когда возвращаемые данные могут быть удалены, попробуйте функцию защитного селектора. Не извлекайте данные напрямую, например:state.todos[props.id].name- Получить сначалаstate.todos[props.id], затем проверьте, существует ли значение, затем попробуйте получить todo.name

  • Поскольку connect добавляет необходимыеSubscriptionКомпонент передается поставщику контекста, а выполнение подписки на подкомпонент откладывается до тех пор, покаconnect()После повторного рендеринга компонента в дереве компонентов поместитеconnect()Компоненты размещаются с помощьюuseSelectorнад компонентами позволит избежать вышеуказанных проблем, еслиconnect()Компонент, который использует ловушки, и дочерний компонент, который запускает повторную визуализацию, вызываются одним и тем же обновлением хранилища.

Уведомление: Если вы хотите получить более подробное описание проблемы,этот чатдетализирует проблему иissue #1179.

представление

Как упоминалось выше, после отправки действияuseSelector()По умолчанию возвращаемое значение функции select сравнивается по ссылке ===, а повторная визуализация запускается только при изменении возвращаемого значения. Однако, в отличие от connect(),useSelector()Это не предотвращает повторный рендеринг дочернего компонента, вызванный повторным рендерингом родительского компонента, даже если реквизиты компонента не изменились.

Если вам нужна аналогичная дальнейшая оптимизация, вы можете захотеть обернуть свои компоненты функций в React.memo() :

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

export const MemoizedCounterComponent = React.memo(CounterComponent)

Рецепт крючков

Мы упростили API хуков по сравнению с исходной альфа-версией, сосредоточившись на меньшем, более простом API. Однако в вашем приложении вы все еще можете использовать некоторые методы, которые мы реализовали ранее. Код в приведенном ниже примере готов для копирования в вашу кодовую базу.

формула:useActions()

Этот хук существует в исходной альфа-версии, но в версии v7.1.0-alpha.4,Dan Abramovбыл удален по предложению . Совет предполагает, что «связывание создателей действий» не так полезно, как раньше, в сценариях, где используются ловушки, и приводит к более концептуальному пониманию и увеличению сложности синтаксиса.

Примечание переводчика:action creatorsТо есть функция, используемая для создания объекта действия.

В компонентах лучше использовать хук useDispatch для получения ссылки на функцию диспетчеризации, а затем вручную вызывать диспетчеризацию (someActionCreator()) или какой-либо желаемый побочный эффект в функции обратного вызова. В своем коде вы все еще можете использоватьbindActionCreatorsФункция для привязки создателей действий или привязки их вручную, например, constboundAddTodo = (text) => dispatch(addTodo(text)).

Однако, если вы хотите использовать этот хук самостоятельно, существует копируемая версия, которая поддерживает передачу создателей действий в виде отдельной функции, массива или объекта.

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