Хуки
Новое API "хуков" в React дают функциональным компонентам возможность использовать локальное состояние компонентов, выполнять побочные действия и многое другое. React также позволяет нам писать кастомные хуки, которые позволяют нам извлекать повторно используемые хуки, чтобы добавить наше собственное поведение поверх встроенных в React хуков.
React Redux включает в себя свой собственное API хуков, которое позволяет вашим компонентам React подписываться на хранилище(store) Redux и отправлять(dispatch) действия.
Подсказка
Мы рекомендуем по умолчанию использовать API хуков React-Redux в ваших компонентах React.
Существующее API connect
по-прежнему работает и будет поддерживаться, но API хуков проще и лучше работает с TypeScript.
Эти хуки впервые были добавлены в версии 7.1.0.
Использование хуков в приложении React Redux
Как и в случае с connect()
, вам следует обернуть все ваше приложение в компонент <Provider>
, чтобы сделать хранилище доступным во всём дереве компонентов:
const store = createStore(rootReducer)
// Начиная с React 18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
Теперь вы можете импортировать любой из перечисленных React Redux хуков и использовать их в своих функциональных компонентах.
useSelector()
const result: any = useSelector(selector: Function, equalityFn?: Function)
Позволяет извлекать данные из состояния(state) хранилища(store) Redux с помощью функции селектора.
Информация
Функция селектора должна быть чистой, поскольку она потенциально может выполняться несколько раз и в произвольные моменты времени.
Функция selector примерно равнозначна аргументу mapStateToProps
для connect
. Функция selector будет вызываться со всем состоянием хранилища Redux в качестве её единственного аргумента. Функция селектор будет запускаться при каждом рендеринге компонента (если только её ссылка не изменилась с момента предыдущего рендеринга компонента, так что кешированный результат может быть возвращен хуком без повторного запуска функции селектора). useSelector()
также будет подписываться на хранилище Redux и запускать ваш селектор всякий раз, когда отправляется(dispatch) действие.
Однако между селекторами, переданными в useSelector()
, и функцией mapState
есть некоторые различия:
- В результате селектор может вернуть любое значение, а не только объект. Возвращаемое значение селектора будет использоваться как возвращаемое значение хука
useSelector()
. - После отправки(dispatch) действия,
useSelector()
выполнит сравнение по ссылке предыдущего значения результата селектора и текущего значения результата. Если они отличаются, компонент будет принудительно перерендерен. Если они совпадают, компонент не будет повторно рендериться. - Функция селектора не получает аргумент
ownProps
. Однако пропсы можно использовать через замыкание (см. примеры ниже) или с помощью каррированного селектора. - При использовании запоминающих селекторов следует проявлять особую осторожность (подробнее см. примеры ниже).
useSelector()
по умолчанию использует строгую проверку равенства ссылок===
, а не с приведением типов (см. следующий раздел для получения дополнительной информации).
Информация
Существуют потенциальные пограничные случаи с использованием пропсов в селекторах, которые могут вызвать проблемы. Дополнительную информацию см. в разделе Предупреждения об использовании на этой странице.
Вы можете вызывать useSelector()
несколько раз в одном компоненте. Каждый вызов useSelector()
создает отдельную подписку на Redux хранилище(store). Из-за группировки обновлений в React (Поведение, используемое в React Redux версии 7), отправленное действие, которое заставляет несколько useSelector()
в одном компоненте возвращать новые значения, должно приводить только к одному повторному рендерингу.
Проверки на равенства и обновления
Когда компонент рендерится, будет вызвана функция селектора, и ее результат будет возвращен
из хука useSelector()
. (Результат кэшируется и возвращается при повторном рендеринге с тем же самым селектором.)
Однако, когда действие отправляется(dispatch) в Redux хранилище(store), useSelector()
вызывает повторную рендеринг только в том случае, если результат селектора отличается от последнего результата. По умолчанию происходит строгое сравнение по ссылке ===
. Этот подход отличается от connect()
, который использует сравнение без приведения типов результатов вызовов mapState
, чтобы определить, нужен ли повторный рендеринг. Отсюда появляется несколько условий использования useSelector()
.
С mapState
все отдельные поля возвращались в объединенном объекте. Вне зависимости имел ли возвращаемый объект новую ссылку или нет - функция connect()
просто сравнила отдельные поля. Когда useSelector()
возвращает новый объект, по умолчанию всегда выполняется повторный рендеринг. Если вы хотите получить несколько значений из хранилища(store), вы можете:
- Вызовите
useSelector()
несколько раз, при этом с каждым вызовом возвращайте одно значение. - Используйте Reselect или аналогичную библиотеку для создания мемоизированный селектора, который возвращает несколько значений в одном объекте, но возвращает новый объект только тогда, когда одно из значений изменилось.
- Используйте функцию
shallowEqual
из React-Redux в качестве аргументаequalityFn
дляuseSelector()
, например:
import { shallowEqual, useSelector } from 'react-redux'
// актуально
const selectedData = useSelector(selectorReturningObject, shallowEqual)
Необязательная функция сравнения также позволяет использовать что-то вроде _.isEqual()
из Lodash или возможности сравнения 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>
}
Использование пропсов через замыкание, чтобы определить, что извлекать
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
с лямбда-функцией селектора, как показано выше, новый экземпляр селектора создается при каждом рендере компонента. Это работает до тех пор, пока селектор не сохраняет какое-либо состояние. Однако мемоизированные селекторы (например, созданные с помощью createSelector
из reselect
) имеют внутреннее состояние, и поэтому при их использовании следует быть осторожным. Ниже вы можете найти типичные сценарии использования мемоизированных селекторов.
Когда селектор зависит только от состояния, просто убедитесь, что он объявлен вне компонента, чтобы один и тот же экземпляр селектора использовался при каждом рендеринге:
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumCompletedTodos = createSelector(
(state) => state.todos,
(todos) => todos.filter((todo) => todo.completed).length
)
export const CompletedTodosCounter = () => {
const numCompletedTodos = useSelector(selectNumCompletedTodos)
return <div>{numCompletedTodos}</div>
}
export const App = () => {
return (
<>
<span>Number of completed todos:</span>
<CompletedTodosCounter />
</>
)
}
То же самое верно, если селектор зависит от пропсов компонента, но будет использоваться только в одном экземпляре одного компонента:
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectCompletedTodosCount = createSelector(
(state) => state.todos,
(_, completed) => completed,
(todos, completed) =>
todos.filter((todo) => todo.completed === completed).length
)
export const CompletedTodosCount = ({ completed }) => {
const matchingCount = useSelector((state) =>
selectCompletedTodosCount(state, completed)
)
return <div>{matchingCount}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<CompletedTodosCount completed={true} />
</>
)
}
Однако, когда селектор используется в нескольких экземплярах компонента и зависит от пропсов компонента, вам необходимо убедиться, что каждый экземпляр компонента получает свой собственный экземпляр селектора (см. здесь для уточнения, почему это необходимо):
import React, { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const makeSelectCompletedTodosCount = () =>
createSelector(
(state) => state.todos,
(_, completed) => completed,
(todos, completed) =>
todos.filter((todo) => todo.completed === completed).length
)
export const CompletedTodosCount = ({ completed }) => {
const selectCompletedTodosCount = useMemo(makeSelectCompletedTodosCount, [])
const matchingCount = useSelector((state) =>
selectCompletedTodosCount(state, completed)
)
return <div>{matchingCount}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<CompletedTodosCount completed={true} />
<span>Number of unfinished todos:</span>
<CompletedTodosCount completed={false} />
</>
)
}
useDispatch()
const dispatch = useDispatch()
Этот хук возвращает ссылку на функцию dispatch
из 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
. Если дочерний компонент пытается оптимизировать поведение рендеринга с помощью React.memo()
или аналогичного, это позволяет избежать ненужного рендеринга дочерних компонентов из-за измененной ссылки колбэка.
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>
))
Информация
Ссылка на функцию dispatch
будет стабильной до тех пор, пока один и тот же экземпляр хранилища(store) передается в <Provider>
.
Обычно этот экземпляр хранилища(store) никогда не изменяется в приложении.
Тем не менее, линтер React не знает об особенности dispatch
быть стабильным, и предупредит, что переменная dispatch
должна быть добавлена в массивы зависимостей для useEffect
и useCallback
. Самое простое решение - добавить их:
export const Todos = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchTodos())
// Безопасно добавляем dispatch в массив зависимостей
}, [dispatch])
}
useStore()
const store = useStore()
Этот хук возвращает ссылку на то же Redux хранилище(store), которое было передано компоненту <Provider>
.
Этот хук не следует использовать часто. Предпочтите useSelector()
в качестве основного выбора. Однако это может быть полезно для менее распространенных сценариев, требующих доступа к хранилищу(store), например, для замены редюсеров(reducer).
Примеры
import React from 'react'
import { useStore } from 'react-redux'
export const CounterComponent = ({ value }) => {
const store = useStore()
// ПРИМЕР! Не делайте так в настоящих проектах.
// Компонент не будет автоматически обновлен, если состояние хранилища(store) изменится
return <div>{store.getState()}</div>
}
Пользовательский контекст
Компонент <Provider>
позволяет указать альтернативный контекст через свойство context
. Это полезно, если вы создаете сложный повторно используемый компонент и не хотите, чтобы ваше хранилище(store) конфликтовало с любым хранилищем Redux, которое могут использовать приложения ваших потребителей.
Чтобы получить доступ к альтернативному контексту через API хуков, используйте функции создания хуков:
import React from 'react'
import {
Provider,
createStoreHook,
createDispatchHook,
createSelectorHook,
} from 'react-redux'
const MyContext = React.createContext(null)
// Экспортируйте свои пользовательские хуки, если хотите использовать их в других файлах.
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>
)
}
Предупреждения при использовании
Устаревшие пропсы и "зомби потомки"
Информация
API хуков React-Redux было готово к работе с тех пор, как мы выпустили его в версии 7.1.0, и мы рекомендуем использовать API хуков в качестве подхода по умолчанию в ваших компонентах. Однако есть несколько крайних случаев, которые могут возникнуть, и мы документируем их, чтобы вы могли о них знать.
На практике это редкая проблема — мы получили гораздо больше комментариев о том, что эти случаи находятся в документации, чем сообщений о реальной проблеме в приложениях.
Одним из самых сложных аспектов реализации React Redux является обеспечение вызова функции mapStateToProps
с «последними» пропсами, когда она определена как (state, ownProps)
. Вплоть до версии 4 сообщалось о повторяющихся ошибках, связанных с исключительными случаями, такими как ошибки, выдаваемые функцией mapState
для элемента списка, данные которого были только что удалены.
Начиная с версии 5, React Redux пытался гарантировать согласованность с ownProps
. В версии 7 это реализовано с помощью пользовательского класса Subscription
внутри connect()
, который формирует вложенную иерархию. Это гарантирует, что подключенные компоненты ниже в дереве будут получать уведомления об обновлении хранилища(store) только после обновления ближайшего подключенного предка. Однако это зависит от того, что каждый экземпляр connect()
переопределяет часть внутреннего контекста React, предоставляя свой собственный уникальный экземпляр Subscription
для формирования вложенности и отображая <ReactReduxContext.Provider>
с этим новым значением контекста.
С хуками невозможно отобразить поставщика контекста, что означает отсутствие вложенной иерархии подписок. Из-за этого проблемы "устаревших пропсов" и "потомка зомби" потенциально могут повторно возникнуть в приложении, использующее хуки вместо connect()
.
В частности, "устаревшие пропсы" означают любой случай, когда:
- функция селектора опирается на пропсы этого компонента для извлечения данных
- родительский компонент будет повторно рендерить и передавать новые пропсы в результате события
- но функция селектора этого компонента выполняется до того, как этот компонент получил возможность повторного рендеринга с этими новыми пропсами
В зависимости от того, какие пропсы использовались и каково текущее состояние хранилища(store), это может привести к возврату неверных данных из селектора или даже к возникновению ошибки.
"Потомок зомби" относится конкретно к случаю, когда:
- При первом проходе встраиваются несколько вложенных подключенных компонентов, в результате чего дочерний компонент подписывается на хранилище(store) раньше, чем его родитель.
- Отправляется(dispatch) действие, которое удаляет данные из хранилища, например, элемент списка дел.
- В результате родительский компонент прекратил рендеринг этого дочернего элемента.
- Однако, поскольку дочерний элемент подписался первым, его подписка выполняется до того, как родитель перестанет отображать ее. Когда он считывает значение из хранилища на основе пропсов, эти данные больше не существуют, и, если логика извлечения не будет безопасной, это может привести к возникновению ошибки.
useSelector()
пытается справиться с этим, перехватывая все ошибки, возникающие при выполнении селектора из-за обновления хранилища (но не когда он выполняется во время рендеринга). При возникновении ошибки компонент будет принудительно отрендерен, и в этот момент селектор будет выполнен снова. Это работает до тех пор, пока селектор является чистой функцией, и вы не зависите от ошибок селектора.
Если вы предпочитаете решать эту проблему самостоятельно, вот несколько возможных вариантов, позволяющих вообще избежать этих проблем с помощью useSelector()
:
- Не полагайтесь на пропсы в вашей функции селектора для извлечения данных.
- В тех случаях, когда вы полагаетесь на пропсы в своей функции селектора и эти пропсы могут меняться со временем, или извлекаемые данные могут быть основаны на элементах, которые могут быть удалены, попробуйте написать функции селектора с защитой. Не обращайтесь напрямую к
state.todos[props.id].name
— сначала прочитайтеstate.todos[props.id]
и убедитесь, что он существует, прежде чем пытаться прочитатьtodo.name
. - Поскольку
connect
добавляет необходимыйSubscription
к поставщику контекста и задерживает оценку дочерних подписок до тех пор, пока подключенный компонент не будет повторно визуализирован, размещение подключенного компонента в дереве компонентов непосредственно над компонентом, использующимuseSelector
, предотвратит эти проблемы, поскольку, пока подключенный компонент повторно отображается, из-за того же обновления хранилища, что и компонент хуков.
Информация
Для более подробное описание этих сценариев см.
Производительность
Как упоминалось ранее, по умолчанию useSelector()
выполняет сравнение равенства ссылок выбранного значения при запуске функции селектора после отправки(dispatch) действия и вызывает повторную визуализацию компонента только в том случае, если выбранное значение изменилось. Однако, в отличие от 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
на основании предложения Дэна Абрамова.
Это предложение было основано на том, что «связывание создателей действий» не так полезно в случае использования на основе хуков и вызывают слишком много концептуальных накладных расходов и синтаксической сложности.
Вероятно, вам следует вызывать хук useDispatch
в ваших компонентах, чтобы получить ссылку на dispatch
, и вручную вызвать dispatch(someActionCreator())
в колбэках и эффектах по мере необходимости. Вы также можете использовать Redux 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)
}
Дополнительные соображения при использовании хуков
Есть некоторые архитектурные компромиссы, которые следует учитывать при принятии решения об использовании хуков или нет. Марк Эриксон хорошо резюмирует их в своих двух постах в блоге Мысли о React хуках, Redux и разделении ответственности и Хуки, HOC и компромиссы.