Как React-Redux принимает решение о перерисовке: селекторы, проверка равенства и типизированные хуки
Руководство по изучению внутреннего устройства React-Redux: Provider, проверка равенства с useSelector, мемоизированные селекторы, функция connect(), типизированные хуки с withTypes() и редкие крайние случаи.
На первый взгляд, архитектура React-Redux кажется очень простой: компонент provider и несколько хуков. Однако в крупных проектах постоянно возникают одни и те же вопросы. Почему этот компонент отрисовывается при каждом вызове dispatch? Почему селектор, возвращающий объект, ведет себя иначе, чем mapStateToProps? Когда shallowEqual является подходящим инструментом, что на самом деле описывают RootState и AppDispatch, и нужен ли batch() в React 18?
В этом руководстве на эти вопросы даются ответы, следуя одной логической нити: что происходит между отправлением действия и повторной отрисовкой. Как только эта цепочка становится понятной, отдельные API перестают казаться списком для запоминания и превращаются в части единой системы, которую можно анализировать, отлаживать и объяснять во время код-ревью или собеседования.
useSelector()
useDispatch()
<Provider />
Текущие рекомендации: разработчики React-Redux советуют использовать API Hooks в качестве стандарта для компонентов. Функция
connect()по-прежнему поддерживается и имеет значение, поскольку от неё зависит множество существующих кодов.
Что такое React-Redux и какова его роль
Redux отвечает за хранение состояния, выполнение редьюсеров и уведомление подписчиков; React отвечает за отрисовку интерфейса. React-Redux — это официальный механизм связи, позволяющий компонентам читать данные из хранилища и запускать обновления, при этом React продолжает отвечать за отрисовку.
Redux
│
│ Application State
▼
React-Redux
│
│ Integration
▼
React
│
│ UI
▼
User
Конкретно, этот механизм связи предоставляет компонентам четыре возможности:
- чтение значений из состояния Redux
- подписка на изменения этих значений
- отправка действий в хранилище
- интеграция обновлений хранилища в цикл отрисовки React
Современные способы реализации этих операций — это три хука:
useSelector()
useDispatch()
useStore()
Плюс компонент, который делает хранилище доступным в первую очередь:
<Provider />
Для более старого кода существует также API высшего порядка для компонентов, которое по-прежнему полностью поддерживается:
connect()
Redux и React-Redux — это разные пакеты
Сам Redux является контейнером состояния и включает в себя следующие концепции:
Store
State
Actions
Reducers
Dispatch
Middleware
Selectors
React-Redux — это лишь мост, и всё, что он экспортирует, связано с взаимодействием с React:
Provider
useSelector
useDispatch
useStore
connect
Когда все слои объединены вместе, они выглядят так:
Redux
┌───────────────┐
│ Store │
│ State │
│ Reducers │
│ Actions │
│ Middleware │
└───────┬───────┘
│
▼
React-Redux
┌───────────────┐
│ Provider │
│ useSelector │
│ useDispatch │
│ useStore │
│ connect │
└───────┬───────┘
│
▼
React
Если вопрос касается того, как меняется состояние (редьюсеры, мидлвары, формат действий), он относится к Redux. Если же речь идет о том, когда компонент замечает изменения или отрисовывается, это относится к React-Redux.
Основной цикл, который нужно запомнить
Прежде чем использовать любой API, необходимо усвоить этот цикл: сначала прочитать селектор, затем обработать взаимодействие, изменить состояние, снова пройтись по селектору и отрендерить содержимое только в случае изменения выбранного значения.
React Component
│
│ useSelector()
▼
Redux Store
│
│ current state
▼
Component
│
│ user interaction
▼
useDispatch()
│
│ dispatch(action)
▼
Reducer
│
▼
New Redux State
│
▼
useSelector()
│
▼
Re-render if
selected value
changed
Почти каждый вопрос, связанный с производительностью в React-Redux, касается именно последнего шага этого цикла.
Provider: обеспечение доступа к хранилищу данных
Хуки могут взаимодействовать только с хранилищем данных, находящимся выше них в иерархии. Компонент <Provider> отвечает за его размещение там. Обычно корень приложения оборачивается им один раз:
import { Provider } from 'react-redux'
import { store } from './store'
function AppRoot() {
return (
<Provider store={store}>
<App />
</Provider>
)
}
По сути, Provider помещает хранилище данных в контекст React. Любой его потомок, независимо от глубины иерархии, может получить к нему доступ без необходимости передачи параметров через цепочку родительских компонентов.
<Provider store={store}>
│
├── App
│ ├── Header
│ ├── Dashboard
│ ├── UserProfile
│ └── Settings
│
└── Every descendant
can use Redux
Если компонент вызывает один из хуков вне Provider, нет никакого хранилища данных для чтения, и хук терпит неудачу (на практике он выбрасывает ошибку с сообщением о отсутствии значения контекста):
useSelector(...)
useDispatch(...)
Часто это приводит к проблемам в единичных тестах, где компонент отрисовывается без оборачивания его в тестовый Provider.
useSelector: как компоненты читают состояние
useSelector() — это хук, который вы будете использовать чаще всего. Вы передаете ему функцию, которая принимает всё состояние Redux и возвращает ту часть, которая нужна компоненту:
const count = useSelector(
state => state.counter.value
)
Структура потока данных проста:
Redux State
│
▼
useSelector()
│
▼
Selected Value
│
▼
React Component
Поскольку React-Redux может вызывать ваш селектор чаще, чем вы ожидаете (при отрисовке, после отправки действий, во время проверок в режиме разработки), селектор должен быть чистым: одинаковый вход — одинаковый выход, без побочных эффектов.
Что делает хук при каждой отрисовке и отправке действия
Возьмем селектор, который выбирает текущего пользователя:
const user = useSelector(
state => state.auth.user
)
За этой одной строкой React-Redux выполняет небольшую процедуру:
- читает текущее состояние хранилища
- передает его в ваш селектор
- сохраняет возвращенное значение как результат «последнего выбора»
- регистрирует подписку на хранилище для этого компонента
- снова запускает селектор после каждого отправленного действия
- сравнивает предыдущий результат с новым
- планирует перерисовку только тогда, когда сравнение показывает их различие
Именно шаг 6 является причиной почти всех неожиданных поведений.
По умолчанию используется строгое сравнение по ссылке
По умолчанию хук
useSelector()
сравнивает результаты с ===:
previousResult === newResult
Когда сравнение дает
true
Селектор не дает React-Redux причин снова отрисовывать компонент. Когда он возвращает
false
компонент планируется к перерисовке.
Это намеренное отличие от connect(), который выполняет поверхностное сравнение объекта, возвращаемого mapStateToProps. Код, который работал нормально с connect(), может начать отрисовываться при каждом действии после переноса на хуки без изменения селектора.
Почему возврат нового объекта приводит к перерисовке каждый раз
Естественная первая попытка получения двух значений выглядит так:
const data = useSelector(state => ({
user: state.user,
count: state.count
}))
С самими значениями всё в порядке, но функция-стрелка создаёт совершенно новый объект каждый раз, когда выполняется:
{
user: ...,
count: ...
}
Два объекта с идентичным содержимым по-прежнему считаются двумя разными объектами, поэтому проверка
oldObject === newObject
всегда даёт результат
false
В результате возникает цепочка вызовов, срабатывающая при каждом действии в приложении, включая те, которые не связаны с пользователями или подсчётами:
Action dispatched
↓
Selector executes
↓
New object created
↓
Different reference
↓
Component re-renders
Это одна из наиболее частых проблем с производительностью в React-Redux. В документации перечислены три решения: использование отдельных селекторов, пользовательская функция равенства вроде shallowEqual или мемоизированный селектор.
Решение первое: вызывайте useSelector один раз на каждое значение
Вместо объединения значений в объект,
const data = useSelector(state => ({
user: state.user,
count: state.count
}))
разделите операцию чтения на отдельные хуки:
const user = useSelector(
state => state.user
)
const count = useSelector(
state => state.count
)
Теперь каждый селектор возвращает значение, уже находящееся в хранилище состояния, а не новую обертку вокруг него. Когда состояние, в котором оно хранится, не изменилось,
user unchanged
count unchanged
ссылки остаются идентичными, и проверка === проходит успешно.
Не существует никакого штрафа за несколько вызовов хука в одном компоненте. Если один dispatch изменяет более одного из выбранных значений, React-Redux группирует полученные обновления, поэтому компонент отрисовывается всего один раз для этого dispatch, а не по одному разу на каждый хук.
Решение два: shallowEqual для целенаправленных объектов
Иногда наиболее подходящим вариантом действительно является возврат объекта, например когда компонент использует небольшой набор связанных полей. В таком случае передайте shallowEqual в качестве функции сравнения:
import {
useSelector,
shallowEqual
} from 'react-redux';
const data = useSelector(
state => ({
user: state.user,
count: state.count
}),
shallowEqual
)
С её помощью React-Redux сравнивает каждое поле верхнего уровня старого и нового объектов с помощью ===, вместо того чтобы сравнивать ссылки на объекты. Новый обёрточный объект с теми же значениями полей считается неизменным.
В новых версиях также возможно указать функцию сравнения через объект параметров:
const data = useSelector(
selector,
{
equalityFn: shallowEqual
}
)
Когда стоит использовать shallowEqual
Используйте shallowEqual, когда группировка значений делает компонент более читаемым. Не рассматривайте его как стандартную функцию, применяемую автоматически при каждом вызове:
useSelector(selector, shallowEqual)
Лучший первый вопрос — может ли компонент просто выбирать каждое значение отдельно. В большинстве случаев это возможно, и результат становится более удобным для просмотра:
const user = useSelector(
state => state.auth.user
)
const permissions = useSelector(
state => state.auth.permissions
)
Имейте в виду, что shallowEqual проверяет только один уровень глубины. Если поле в возвращаемом объекте само по себе является новым массивом или объектом, сравнение всё равно провалится.
Селекторы, вычисляющие данные
Селекторы выполняют не только извлечение полей. Именно в них также вычисляются производные данные, такие как отфильтрованный список:
const selectCompletedTodos = state =>
state.todos.filter(
todo => todo.completed
)
Проблема в том, что
filter()
всегда выделяется новый массив. Даже когда список задач не изменился никаким существенным образом,
oldArray !== newArray
Он хранит данные, и компонент отрисовывается после каждого действия. Мемоизация решает эту проблему путем кэширования результата на основе входных данных. Если входные данные совпадают с прошлыми, возвращается уже сохраненный результат:
Same inputs
↓
Return cached result
Когда входные данные меняются, вычисления выполняются заново:
Changed inputs
↓
Recalculate
createSelector из Reselect
Стандартным инструментом для этого является функция createSelector из библиотеки Reselect (она переэкспортируется в Redux Toolkit). Вы указываете селекторы входных данных, а затем функцию-результат, которая выполняется только при изменении этих селекторов:
import { createSelector } from 'reselect';
const selectCompletedTodos = createSelector(
state => state.todos,
todos =>
todos.filter(todo => todo.completed)
)
Компонент использует её как любой другой селектор:
const todos = useSelector(
selectCompletedTodos
)
Поскольку state.todos представляет собой тот же самый ссылочный массив, селектор возвращает его предыдущий отфильтрованный массив, поэтому оператор === возвращает истину. Однако существует нюанс: мемоизированный селектор хранит кэш, поэтому место его создания имеет значение.
Создавайте мемоизированные селекторы, зависящие только от состояния, один раз на уровне модуля
Когда мемоизированный селектор зависит только от состояния Redux,
const selectCompletedTodos = createSelector(
state => state.todos,
todos => ...
)
объявляйте его вне любого компонента:
const selectCompletedTodos = createSelector(...)
и обращайтесь к нему из компонента:
function TodoList() {
const todos = useSelector(
selectCompletedTodos
)
}
Если функция createSelector выполняется внутри тела компонента, при каждом отрисовке создается новый селектор с пустым кэшем, и мемоизация не будет полезна. Инстанция на уровне модуля сохраняется между отрисовками.
Мемоизированные селекторы, которые также нуждаются в параметрах
Простые, немемоизированные селекторы, которые читают параметры, не представляют опасности. Они не хранят кэш, поэтому здесь нет возможности допустить ошибку:
function TodoListItem({ id }) {
const todo = useSelector(
state => state.todos[id]
)
return <div>{todo.text}</div>
}
Ситуация становится более сложной, когда мемоизированный селектор зависит одновременно и от состояния, и от параметров
Redux State + Component Props
Такой селектор кэширует результат для своих последних аргументов. Если множество элементов списка используют один экземпляр, но имеют разные id, они постоянно аннулируют кэш друг друга. Для одного компонента обычно достаточно создать селектор с помощью useMemo; для множества компонентов необходимо знать стратегию мемоизации вашей библиотеки (размер кэша или один экземпляр на компонент). Этот вопрос часто возникает на собеседованиях для старших специалистов и при работе с длинными списками.
Селекторы должны оставаться чистыми
Селектор должен быть простой функцией состояния:
State
↓
Selector
↓
Value
и никогда не должен служить местом, куда «утекает» внешняя логика:
State
↓
Selector
↓
API call
↓
Mutation
↓
Side effect
Следует избегать таких селекторов; логирование, сетевые запросы и изменения данных не должны находиться здесь:
const selectUser = state => {
console.log('side effect')
// API call ❌
// mutation ❌
return state.user
}
Использование props внутри селектора
Селектор, определенный непосредственно внутри компонента, может просто использовать props этого компонента:
function TodoItem({ id }) {
const todo = useSelector(
state => state.todos[id]
)
return <div>{todo.text}</div>
}
Значение передается от свойства к селектору через замыкание:
id
↓
closure
↓
selector
↓
state.todos[id]
Это существенная разница по сравнению с mapStateToProps, который принимает ownProps в качестве второго аргумента. useSelector() совсем не передает свойства, поэтому приходится полагаться на замыкания или фабрики селекторов, принимающие дополнительные аргументы.
useDispatch: отправка действий
Если useSelector() отвечает за чтение данных,
useSelector
↓
READ
то useDispatch() отвечает за запись данных:
useDispatch
↓
DISPATCH
Он возвращает функцию dispatch хранилища, которую вызывают из обработчиков событий:
const dispatch = useDispatch()
function handleClick() {
dispatch(increment())
}
или непосредственно в JSX:
<button
onClick={() => dispatch(increment())}
>
Increment
</button>
Что запускается при вызове dispatch
Пошаговый анализ процесса нажатия кнопки подтверждает основной цикл, рассмотренный ранее:
User clicks button
↓
dispatch(action)
↓
Redux
↓
Reducer
↓
New state
↓
useSelector()
↓
Component updates
Конкретный вызов с создателем действия и нагрузкой выглядит так:
dispatch(
addTodo({
id: 1,
text: 'Learn Redux'
})
)
Стабильные обратные вызовы для мемоизированных дочерних элементов
Большинству обратных вызовов при отправке сигналов не требуется использование useCallback. Исключение составляют случаи, когда обработчик передается
const increment = () =>
dispatch(incrementAction())
дочернему элементу, обёрнутому в React.memo:
<MyButton
onIncrement={increment}
/>
Поскольку функция-стрелка создаётся заново при каждой отрисовке родителя, мемоизированный дочерний элемент каждый раз получает новое свойство и всё равно отрисовывается. Обёртка обработчика решает эту проблему:
const increment = useCallback(
() => dispatch(incrementAction()),
[dispatch]
)
вместе с мемоизированным дочерним элементом:
const MyButton = React.memo(...)
Указание dispatch в качестве зависимости безопасно: его идентичность остаётся прежней, пока компонент Provider получает тот же экземпляр хранилища.
useStore: прямой доступ, редко нужен
Третий хук предоставляет вам сам объект хранилища:
const store = useStore()
С его помощью можно вызывать прямые методы хранилища:
store.getState()
store.dispatch(...)
store.subscribe(...)
Такой способ доступа почти никогда не должен использоваться компонентом для отображения данных. Чтение происходит через
useSelector()
а запись — через
useDispatch()
В документации useStore() рассматривается как выход из положения в редких случаях, например при внедрении редьюсера, а не для обычных операций чтения.
Почему значение store.getState() в функции render устаревает
Рассмотрим компонент, который читает данные непосредственно из хранилища:
function Component() {
const store = useStore()
const user = store.getState().user
return <div>{user.name}</div>
}
Он отображается правильно один раз, а затем отстает. getState() — это однократное чтение без подписки, поэтому последующие изменения никогда не заставляют React перерисовать этот компонент. Версия с подпиской остается синхронизированной:
const user = useSelector(
state => state.user
)
Три хука в одном взгляде
┌────────────────────────────┐
│ React-Redux Hooks │
├────────────────────────────┤
│ useSelector() │ → READ
│ useDispatch() │ → DISPATCH
│ useStore() │ → STORE ACCESS
└────────────────────────────┘
В повседневном коде компонентов первые два элемента выполняют практически всю работу:
90%+
useSelector()
useDispatch()
useStore() используется лишь эпизодически.
connect(): API высших порядков для компонентов
Хуками рекомендуется пользоваться, но connect() по-прежнему существует, и многие долговечные кодбазы построены на нем. Можно столкнуться с кодом вроде этого:
connect(
mapStateToProps,
mapDispatchToProps
)(Component)
Как connect преобразует данные хранилища в свойства
Ментальная модель заключается в использовании обертки, которая преобразует данные хранилища и функции отправки команд в обычные свойства:
Redux Store
│
▼
connect()
│
├── mapStateToProps
│
└── mapDispatchToProps
│
▼
Component Props
mapStateToProps принимает состояние и возвращает объект свойств:
const mapStateToProps = state => ({
user: state.auth.user,
count: state.counter.value
})
Фактически он выполняет следующее преобразование:
Redux State
↓
Component Props
Обернутый компонент остается обычной функцией, зависящей от своих свойств, и не знает о существовании Redux:
function User({ user, count }) {
return (
<div>
{user.name}
{count}
</div>
)
}
mapDispatchToProps предоставляет функции-обратные вызовы. В своей функциональной форме вы получаете dispatch и сами создаете обработчики:
const mapDispatchToProps =
dispatch => ({
increment: () =>
dispatch(increment())
})
Затем компонент вызывает их в качестве свойств:
props.increment()
Сокращенная форма mapDispatchToProps
Более краткая форма передает объект создателей действий:
const mapDispatchToProps = {
increment,
decrement
}
React-Redux привязывает каждого создателя действий так, что при вызове свойства происходит его выполнение. Обычно это более аккуратный вариант.
Четыре распространенных варианта подключения
Вы столкнетесь с четырьмя вариациями. При отсутствии аргументов компонент получает в качестве свойства только dispatch:
connect()(Component)
При наличии только маппера состояния компонент считывает данные, но не получает привязанных создателей действий:
connect(
mapStateToProps
)(Component)
При использовании null в качестве первого аргумента компонент никогда не слушает хранилище данных и принимает только параметры dispatch:
connect(
null,
mapDispatchToProps
)(Component)
А при использовании обоих аргументов он одновременно считывает данные и отправляет команды dispatch:
connect(
mapStateToProps,
mapDispatchToProps
)(Component)
Важно знать, что использование null позволяет пропустить процедуру подписки: это недорогой способ предоставить компоненту возможность отправки команд dispatch без его перерисовки при обновлении хранилища данных.
connect возвращает новый компонент
Вызов
connect(
mapStateToProps,
mapDispatchToProps
)(MyComponent)
не изменяет MyComponent. Он создает отдельный обёрточный компонент, внутри которого отображается исходный компонент:
MyComponent
│
▼
connect()
│
▼
ConnectedComponent
Именно поэтому модули, использующие connect, обычно экспортируют обёрнутую версию в качестве стандартной, а иногда отдельно экспортируют чистый компонент для тестирования.
Выбор между хуками и connect
Для нового приложения ответ прост:
New React application
↓
Hooks
Для уже существующего решения ответ прост и прагматичен:
Existing connect()
↓
Understand and maintain it
Хуки позволяют сократить количество шаблонного кода, исключают необходимость в компонентах-обертках и значительно упрощают работу с TypeScript, поэтому они являются стандартом. Компоненты, использующие connect(), не требуют переписывания; их следует преобразовать при любом изменении.
Разница в проверке равенства за одну строку
Помните об этой паре:
useSelector()
↓
=== reference equality
против
connect()
↓
shallow equality
Многие ошибки вида «раньше всё работало до рефакторинга» возникают из-за этого: свежий объект был безопасен в mapStateToProps, но не проходит проверку === в useSelector().
Определение типов для React-Redux с использованием TypeScript
React-Redux поставляет собственные определения типов, а документация описывает стандартную настроку с использованием типов. Она основана на шести ключевых понятиях:
RootState
AppDispatch
AppStore
useAppSelector
useAppDispatch
useAppStore
RootState: определение типа состояния на основе хранилища
Если у вас есть магазин, настроенный с использованием Redux Toolkit,
const store = configureStore({
reducer: {
counter: counterReducer,
users: usersReducer
}
})
вы можете получить тип состояния из того, что возвращает getState:
export type RootState =
ReturnType<typeof store.getState>
Это лучше, чем вручную определять структуру состояния,
type RootState = {
counter: CounterState
users: UsersState
}
потому что автоматически определённый тип автоматически учитывает редьюсеры.
AppDispatch: тип диспача, включая мидлвары
Тип диспача можно определить аналогичным образом:
export type AppDispatch =
typeof store.dispatch
Простой тип Redux Dispatch поддерживает только простые действия; автоматически определённый тип учитывает наличие мидлваров, что важно при использовании:
- мидлваров, изменяющих параметры, принимаемые
dispatch - функций-танков
- переопределённого диспача
- асинхронных действий любого типа
Без AppDispatch вызов танка приведёт к ошибке типа.
AppStore: тип самого магазина
Тип магазина можно определить ещё одним шагом инференции:
export type AppStore =
typeof store
Благодаря этому у каждой проблемы есть единый источник истины. Тип состояния:
RootState
↓
state type
Тип диспетчера:
AppDispatch
↓
dispatch type
Тип хранилища:
AppStore
↓
store type
AppStore особенно удобен, когда необходимо создавать хранилище для каждого запроса или теста и передавать его дальше.
Заранее определенные хуки с withTypes()
Начиная с React-Redux 9.1.0, каждый хук предоставляет метод .withTypes():
useDispatch.withTypes()
useSelector.withTypes()
useStore.withTypes()
Описанный паттерн позволяет один раз создать хуки, специфичные для конкретного приложения:
export const useAppDispatch =
useDispatch.withTypes<AppDispatch>()
export const useAppSelector =
useSelector.withTypes<RootState>()
export const useAppStore =
useStore.withTypes<AppStore>()
Что дают типизированные хуки
Без них каждому селектору требуется явная аннотация:
const user = useSelector(
(state: RootState) =>
state.auth.user
)
С типизированным хуком,
const user = useAppSelector(
state => state.auth.user
)
компилятор уже знает
state = RootState
Часть, отвечающая за диспетчеринг, работает так же:
const dispatch = useAppDispatch()
Этот метод dispatch принимает функции-обертки и всё остальное, что разрешает ваш middleware, с полной проверкой.
Файл hooks.ts для приложения
В типичном проекте всё это хранится в одном небольшом модуле:
import {
useDispatch,
useSelector,
useStore
} from 'react-redux'
import type {
RootState,
AppDispatch,
AppStore
} from './store'
export const useAppDispatch =
useDispatch.withTypes<AppDispatch>()
export const useAppSelector =
useSelector.withTypes<RootState>()
export const useAppStore =
useStore.withTypes<AppStore>()
Компоненты импортируют данные из этого модуля вместо прямого импорта из react-redux:
const user = useAppSelector(
state => state.auth.user
)
const dispatch = useAppDispatch()
ConnectedProps для типизированного кода connect()
Кодовые базы, в которых компоненты connect() типизированы, будут содержать
ConnectedProps
Разделите вызов connect сначала на создание коннектора, а затем извлеките свойства, которые он вводит:
const connector = connect(
mapState,
mapDispatch
)
type PropsFromRedux =
ConnectedProps<typeof connector>
Функция PropsFromRedux точно описывает, что вводит коннектор, поэтому типы никогда не дублируются.
Проектирование хороших селекторов
Легко подумать, что селектор — это не более чем
state => state.user
В более крупных приложениях селекторы лучше рассматривать как границу, преобразующую формат хранения состояния в тот вид, который требуется интерфейсу:
Redux State
↓
Selector
↓
UI-friendly data
Например, правило отображения списка задач может находиться в отдельной функции с именем:
const selectVisibleTodos =
state =>
state.todos.filter(
todo => !todo.hidden
)
а компонент просто запрашивает результат её работы:
const todos = useSelector(
selectVisibleTodos
)
Компонент остается представительским, а правило можно тестировать отдельно.
Хороший селектор — это точный
const selectUserName =
state => state.auth.user.name
Он возвращает именно то, что отображает компонент, и ничего больше, поэтому вызывает перерисовку только тогда, когда меняется это имя.
Выбор всего состояния почти всегда неверен
const selectEverything =
state => state
Redux генерирует новый объект корневого состояния каждый раз, когда какой-либо редьюсер что-то изменяет. Селектор, возвращающий корневое состояние, таким образом возвращает новый ссылочный объект практически после каждого действия:
Anything in Redux changes
↓
Root state reference changes
↓
Selector result changes
↓
Component re-renders
В режиме разработки React-Redux фиксирует такой паттерн.
Сохраняйте селекторы детализированными
Предпочитайте несколько целенаправленных запросов:
const count =
useSelector(
state => state.counter.value
)
const user =
useSelector(
state => state.auth.currentUser
)
вместо одного общего запроса:
const state =
useSelector(state => state)
Полезное правило:
Выбирайте наименьший фрагмент состояния, который компонент действительно может использовать.
Проверки селекторов в режиме разработки
В новых версиях React-Redux при сборке в режиме разработки выполняются дополнительные проверки селекторов. Два из них стоит знать по имени.
Проверка стабильности
В первой проверке селектор вызывается второй раз с тем же состоянием, и результаты сравниваются:
selector(state)
↓
run again with same state
↓
same result?
Если результат
same reference
селектор является стабильным. Если же нет
new reference
React-Redux выводит предупреждение, поскольку селектор, возвращающий новый ссылочный объект для идентичных данных входа, будет перерисовывать свой компонент при каждом обновлении хранилища данных.
Типичным примером является селектор в виде объекта-литерала, описанный ранее:
const data = useSelector(
state => ({
count: state.count,
user: state.user
})
)
Поскольку объект пересоздается при каждом вызове, проверка выявляет:
same input
↓
different object
↓
unstable selector
Настройка частоты выполнения проверок
Вы можете установить частоту выполнения для всего приложения в компоненте Provider:
<Provider
store={store}
stabilityCheck="always"
>
<App />
</Provider>
или переопределить её для отдельного вызова хука:
const count = useSelector(
selectCount,
{
devModeChecks: {
stabilityCheck: 'once'
}
}
)
Допустимые значения:
never
once
always
По умолчанию значение — 'once', что означает, что проверка выполняется при первом вызове каждого хука. Ничего из этого не выполняется в продакшн-версиях приложения.
Проверка функции идентичности
Вторая проверка ищет селектор, который возвращает свои входные данные без изменений:
state => state
В компоненте это выглядит так:
const state = useSelector(
state => state
)
Это связывает компонент с каждым изменением хранилища данных:
Any Redux change
↓
Root state changes
↓
Component re-renders
В документации это называется проверкой функции идентичности. В более ранних версиях она называлась noopCheck, и вы можете встретить это название в старых конфигурациях.
Решение такое же, как и раньше: замените
const state = useSelector(
state => state
)
на чтение конкретных значений, которые вам нужны:
const count = useSelector(
state => state.counter.value
)
const user = useSelector(
state => state.auth.currentUser
)
Отрисовка и производительность за пределами селекторов
Отрисовка родителя всё ещё каскадируется
useSelector() управляет только теми отрисовками, которые вызваны обновлениями хранилища. Он не влияет на обычное правило React, согласно которому компонент отрисовывается при отрисовке его родителя:
Parent renders
↓
Child renders
Это происходит даже в том случае, если состояние Redux совсем не изменилось. Когда дочерний компонент требует много ресурсов, а его параметры стабильны, оберните его в
React.memo()
Это отличается от connect(), чей обертывающий компонент ведет себя как мемизированный элемент; компоненты, основанные на хуках, не получают такой функциональности бесплатно.
Сочетание React.memo с useSelector
Здесь компонент подписывается на счётчик и одновременно мемизируется по свойству name:
const Counter = ({ name }) => {
const count = useSelector(
state => state.counter.value
)
return (
<div>
{name}: {count}
</div>
)
}
export default React.memo(Counter)
Эти два механизма покрывают два источника перерисовки:
Redux selector
+
React.memo
↓
More controlled rendering
Мемизация имеет свои затраты, поэтому сначала проведите профилирование. Подробнее о различных факторах, вызывающих перерисовку, см. нашу статью о распространённых паттернах, приводящих к ненужной перерисовке в React.
Модель производительности, которая помещается на один экран
После каждого действия React-Redux перезапускает селекторы подписанных компонентов, сравнивает каждый результат с предыдущим и отрисовывает только те компоненты, чьи результаты отличаются:
Redux action
↓
Store updates
↓
Selectors execute
↓
Selector results compared
↓
Changed?
┌───┴────┐
No Yes
│ │
│ ▼
│ Re-render
│
└── No Redux-triggered render
Ваша задача — написать селекторы, которые:
- возвращают только те данные, которые использует компонент
- возвращают стабильные ссылки, когда ничего значимого не изменилось
- не создают новые объекты или массивы без необходимости
- мемоизируют данные, вычисление которых требует много ресурсов
Правило первое: никогда не выбирать корневое состояние
useSelector(state => state)
Правило второе: избегать создания обёрточных объектов в селекторах
Подобный селектор создаёт новые объекты при каждом запуске:
useSelector(state => ({
user: state.user,
count: state.count
}))
Возвращайте такой объект только тогда, когда вы намеренно используете его вместе с
shallowEqual
или передаёте его через мемоизированный селектор.
Правило третье: мемоизировать ресурсоёмкие вычисления
Сортировка, фильтрация, группировка и объединение относятся к
createSelector(...)
Правило четвертое: используйте React.memo для нужных компонентов
Прежде чем мемоизировать компонент, пройдитесь по краткому списку проверок:
Is the component expensive?
↓
Does it receive stable props?
↓
Does it re-render unnecessarily?
↓
Then consider React.memo()
Если в вашей кодовой базе используется React Compiler, большая часть такого ручного мемоизирования уже обрабатывается автоматически.
Правило пятое: выбирайте наименьший возможный диапазон для селектора
Слишком широкий диапазон:
state => state
Лучше:
state => state.auth.user
Еще лучше, когда компонент отображает только название:
state => state.auth.user.name
Редкие крайние случаи: устаревшие параметры и «зомби-дети»
Большинство приложений никогда не сталкиваются с этими двумя проблемами, но они объясняют, почему полезно использовать защитные селекторы.
Устаревшие параметры
Для решения проблемы устаревших параметров необходим селектор, зависящий от конкретного параметра, а также обновление хранилища данных, изменяющее как состояние, так и этот параметр косвенно:
Selector depends on component props
↓
Redux action updates state
↓
Parent would receive new props
↓
Child selector runs first
↓
Selector sees old props
Подписка дочернего элемента может сработать раньше, чем родительский элемент перерисуется и передаст новые параметры. На короткое время селектор сочетает в себе актуальное состояние и старые параметры. Типичная ситуация выглядит так:
const todo = useSelector(
state => state.todos[props.id]
)
Если элемент с указанным id только что был удален или родительский элемент собирается передать другой id, селектор временно читает данные, которые уже не соответствуют действительности.
Создание селекторов, устойчивых к отсутствию данных
Нестабильная версия предполагает, что элемент всегда существует:
state.todos[props.id].name
Защитная версия сначала пытается найти элемент,
const todo =
state.todos[props.id]
а только затем читает из него данные:
return todo
? todo.name
: undefined
Решение заключается в простом проверочном условии:
Does data exist?
↓
Yes → use it
No → handle safely
Опциональное цепление (state.todos[id]?.name) выражает ту же идею в одном операторе.
«Зомби»-дочерние элементы
Сценарий с «зомби-ребёнком» включает родителя, отвечающего за отображение списка, и ребёнка, подписанного на один из элементов этого списка:
Parent
│
└── Child
Последовательность действий выглядит следующим образом:
- Ребёнок подписывается на хранилище данных.
- Какое-то действие удаляет данные, которые отображает ребёнок.
- При следующем отрисовывании родитель перестаёт отображать этого ребёнка.
- До того, как это произойдёт, срабатывает подписка ребёнка.
- Селектор ребёнка пытается получить данные, которых уже нет.
На этом последнем шаге возникает ошибка из-за незащищённого селектора. React-Redux имеет механизмы для обработки ошибок селекторов, вызванных обновлениями хранилища, но защитные селекторы остаются наиболее надёжным решением.
Почему хуки более уязвимы, чем connect
connect() создаёт вложенную иерархию подписок: каждый связанный компонент обновляется только после того, как это сделают его связанные предки, что обеспечивает порядок сверху вниз. Хуки подключаются непосредственно к хранилищу без такой иерархии, поэтому гарантии порядка слабее, и такие крайние случаи становятся теоретически возможными.
Рассматривайте это как фоновые знания, а не причину для избегания хуков. Официальная позиция заключается в том, что эти проблемы редко встречаются в реальных приложениях.
Расширенные функции Provider
Персонализированный контекст для изолированных хранилищ
По умолчанию,
<Provider store={store}>
хранилище публикуется через встроенный контекст React-Redux. Библиотека компонентов, которая использует Redux внутри себя, может столкнуться с конфликтом с хранилищем основного приложения. Чтобы избежать этого, Provider принимает собственный контекст:
<Provider
context={MyContext}
store={myStore}
>
Хуки, привязанные к этому контексту, поступают из функций-фабрик:
createStoreHook()
createDispatchHook()
createSelectorHook()
Это особенно важно для переиспользуемых библиотек, где иначе могли бы возникнуть конфликты между различными хранилищами данных.
batch() и автоматическое группирование операций в React 18
В старом коде часто группируют последовательные вызовы dispatch() с помощью batch(), чтобы React отрисовывал интерфейс один раз вместо двух:
batch(() => {
dispatch(action1())
dispatch(action2())
})
В React 18 обновления автоматически группируются, включая те, которые происходят в рамках обещаний и таймеров, поэтому типичному приложению React 18 не требуется использование batch() в этих случаях. В новых версиях React-Redux оно сохраняется в основном ради совместимости; проверьте актуальную документацию и ожидайте его наличия в старом коде.
serverState для серверной отрисовки и гидратации
Для серверной отрисовки компонент Provider принимает дополнительное свойство:
<Provider
store={store}
serverState={preloadedState}
>
Сервер генерирует HTML из начального состояния и отправляет это состояние браузеру. Параметр serverState обеспечивает использование того же снимка состояния при процессе гидратации, что предотвращает несоответствия:
Server
↓
Initial Redux State
↓
HTML
↓
Browser Hydration
↓
Provider(serverState)
↓
Consistent initial render
Типичная структура проекта
Приложения на TypeScript, построенные с использованием Redux Toolkit, часто организованы по функциональным блокам, причем настройки хранилища состояния сосредоточены в одном месте:
src/
│
├── app/
│ ├── store.ts
│ └── hooks.ts
│
├── features/
│ │
│ ├── counter/
│ │ ├── counterSlice.ts
│ │ └── Counter.tsx
│ │
│ ├── users/
│ │ ├── usersSlice.ts
│ │ └── Users.tsx
│ │
│ └── auth/
│ ├── authSlice.ts
│ └── Login.tsx
│
├── App.tsx
└── main.tsx
store.ts
Модуль хранилища состояния настраивает редьюсеры и экспортирует соответствующие типы:
const store = configureStore({
reducer: {
counter: counterReducer,
users: usersReducer,
auth: authReducer
}
})
export type RootState =
ReturnType<typeof store.getState>
export type AppDispatch =
typeof store.dispatch
export type AppStore =
typeof store
hooks.ts
Модуль хуков преобразует эти типы в хуки приложения:
export const useAppDispatch =
useDispatch.withTypes<AppDispatch>()
export const useAppSelector =
useSelector.withTypes<RootState>()
export const useAppStore =
useStore.withTypes<AppStore>()
Компонент, использующий оба подхода
Компонент-счётчик работает, читая и записывая данные исключительно через типизированные хуки:
function Counter() {
const count = useAppSelector(
state => state.counter.value
)
const dispatch = useAppDispatch()
return (
<>
<span>{count}</span>
<button
onClick={() =>
dispatch(increment())
}
>
+
</button>
</>
)
}
Поток выполнения в этом компоненте полностью соответствует основному циклу с самого начала:
Component
│
├── useAppSelector()
│ ↓
│ READ
│
└── useAppDispatch()
↓
DISPATCH
↓
Redux
↓
New State
↓
useAppSelector()
↓
Component
Роль Redux Toolkit
Redux Toolkit и React-Redux дополняют друг друга, а не являются альтернативами:
Redux Toolkit
+
React-Redux
Redux Toolkit улучшает работу с Redux: настройку хранилища, редьюсеры, асинхронную логику, мемоизированные селекторы и загрузку данных.
configureStore
createSlice
createAsyncThunk
createSelector
RTK Query
React-Redux продолжает отвечать за связь с React:
Provider
useSelector
useDispatch
useStore
connect
Официальный гайд React-Redux для быстрого старта настраивает их вместе, и именно такая комбинация используется по умолчанию в новых проектах.
Полная схема в одной диаграмме
Собираем все компоненты воедино, начиная с элемента Provider и заканчивая проверкой равенства:
React
│
▼
<Provider>
│
▼
Redux Store
│
┌────────┴────────┐
│ │
useSelector() useDispatch()
│ │
│ ▼
│ Action
│ │
│ ▼
│ Reducer
│ │
│ ▼
│ New State
│ │
└─────────┬───────┘
▼
Selector runs
│
▼
Equality check
│
┌──────┴──────┐
│ │
Same Different
│ │
▼ ▼
No Redux Re-render
render
Распространенные ошибки и способы их выявления
Подписка на весь состояние
useSelector(state => state)
Замените её на конкретные селекторы.
Заключение значений в новый объект
useSelector(state => ({
user: state.user
}))
Разделите это на отдельные хуки или намеренно используйте функцию shallowEqual.
Фильтрация или преобразование внутри селектора при каждом запуске
useSelector(state =>
state.todos.filter(...)
)
Если это выполняется часто или с большими списками, переместите его в мемизированный селектор.
Чтение данных рендеринга через useStore
Вызов
store.getState()
в логике рендеринга позволяет получить значение без подписки. Используйте
useSelector()
чтобы компонент обновлялся при изменении значения.
Прямое изменение состояния
Приписка вроде
state.user.name = 'John'
нарушает принцип неизменяемости обновлений в Redux: ссылки не меняются, поэтому селекторы видят «никаких изменений», и компоненты не рендерятся. (В редьюсерах из функции createSlice Redux Toolkit такой подход допускается, поскольку Immer преобразует его в неизменяемое обновление; во всех остальных случаях это ошибка.)
Побочные эффекты внутри селекторов
state => {
fetch(...)
return state.user
}
Селектор должен вычислять и возвращать только результат, ничего больше.
Запоминание по привычке
Распространение этого подхода повсюду
useMemo()
useCallback()
React.memo()
это увеличивает сложность и нагрузку на сравнения без доказательств того, что это помогает. Оптимизируйте уже измеренный процесс отрисовки.
Неправильное размещение экземпляров селекторов, сохраненных в кэше
Селектор с кэшем ведет себя по-разному в зависимости от своего контекста. Прежде чем им пользоваться, убедитесь, что он является:
global
per component
per component instance
Общий экземпляр, используемый с множеством разных аргументов, может так и не воспользоваться своим кэшем.
Вопросы на собеседовании с краткими ответами
Что такое React-Redux и зачем нужен Provider?
Это официальное связующее звено React и Redux: Provider, хуки и connect позволяют компонентам читать состояние, реагировать на изменения и отправлять действия. Provider помещает хранилище состояния в контекст React, чтобы любой его потомок мог к нему обратиться.
useSelector против useDispatch?
Один из них используется для чтения:
useSelector
↓
READ Redux state
Другой вариант написания:
useDispatch
↓
DISPATCH Redux actions
Как useSelector запускает перерисовку?
После каждого действия он снова выполняет выборник и сравнивает результат с предыдущим с помощью === или функции равенства, которую вы передали. Перерисовка запускается только при наличии разницы.
Почему этот выборник постоянно запускает перерисовку, и как это исправить?
useSelector(state => ({
user: state.user,
count: state.count
}))
На каждой итерации создается новый объект, поэтому проверка ссылок всегда проваливается. Три способа решения:
1. Multiple useSelector calls
2. shallowEqual
3. Memoized selector
useSelector против connect?
Хуки сравнивают результаты выборников по ссылкам; connect() выполняет поверхностное сравнение свойств из mapStateToProps. Хуки являются стандартом, connect() по-прежнему поддерживается.
Зачем использовать мемоизированные выборники?
Они пересчитывают полученные данные только тогда, когда меняются входные данные, в остальных случаях возвращают тот же объект ссылки. Классическим примером является фильтр:
todos
↓
filter completed
↓
new array
Что такое RootState, AppDispatch и withTypes()?
Выведенный тип всего состояния:
type RootState =
ReturnType<typeof store.getState>
Выведенный тип диспетчера, включая промежуточные компоненты вроде thunks:
type AppDispatch =
typeof store.dispatch
А также вспомогательные функции, которые возвращают хуки, заранее привязанные к этим типам:
useDispatch.withTypes<AppDispatch>()
useSelector.withTypes<RootState>()
useStore.withTypes<AppStore>()
Зачем нужны типизированные хуки?
Они заменяют повторяющиеся аннотации вроде
useSelector(
(state: RootState) =>
state.user
)
на
useAppSelector(
state => state.user
)
Почему селекторы должны быть чистыми?
Селектор может выполняться несколько раз для одного и того же состояния — во время отрисовки, после каждой операции и в моменты, которые не контролируются кодом. Любой побочный эффект будет срабатывать непредсказуемое количество раз.
Для чего нужен useStore?
Редкие случаи, когда действительно требуется объект хранилища. Чтение состояния для отрисовки происходит через useSelector().
Что такое «зомби-дети» и устаревшие атрибуты?
Оба явления представляют собой редкие проблемы с порядком обработки. «Зомби-дети» продолжают обрабатывать обновления до того, как их родитель удаляет их, и читают удаленные данные; устаревшие атрибуты означают, что селектор, зависящий от атрибутов, выполняется с актуальным состоянием, но со старыми атрибутами. Защитные селекторы позволяют решить обе проблемы.
Требуется ли всё ещё функция batch() в React 18?
Как правило, нет, поскольку React 18 автоматически группирует операции, но вы всё ещё можете столкнуться с ней в более старом коде.
Почему функции connect и хуки могут отрисовывать контент по-разному?
Из-за различий в моделях подписки и способах сравнения:
connect()
↓
shallow equality
против
useSelector()
↓
strict === equality
Почему функция useSelector выполняется так часто?
Она:
- выполняется во время отрисовки
- слушает изменения в хранилище
Таким образом, встроенный селектор, являясь новой функцией при каждой отрисовке, выполняется при каждой отрисовке; стабильная ссылка на селектор позволяет React-Redux пропустить это вызов.
Что изучать в порядке приоритета
Уровень один: обязательно должно быть знакомо
Provider
useSelector
useDispatch
Redux Store flow
Selectors
=== equality
Re-render behavior
Redux Toolkit + React-Redux
TypeScript
RootState
AppDispatch
.withTypes()
Уровень два: прочные практические знания
shallowEqual
Memoized selectors
createSelector
useStore
connect
mapStateToProps
mapDispatchToProps
ConnectedProps
React.memo
Уровень три: продвинутые темы
Stale props
Zombie children
Selector + props
Selector memoization
Custom context
Development mode checks
SSR serverState
batch()
Что не стоит запоминать
Вам не нужно изучать документацию строчка за строчкой. Можно пропустить:
- способ внутренней реализации механизма подписки
- редко используемые опции
connect() - давно устаревшие паттерны
- детали реализации исходного кода
Важен логический подход к правилам. Знание фактов
useSelector uses ===
менее полезно, чем способность отвечать на вопросы
Why?
а именно:
Because returning a new object
creates a new reference.
это приводит к определенной цепочке действий
New reference
↓
=== false
↓
re-render
Если вы понимаете эту цепочку, вы сможете сами вывести большинство остальных правил.
Десять правил, которые следует соблюдать
1. Чтение данных с использованием useSelector
useSelector()
это способ, с помощью которого компоненты читают состояние.
2. Запись данных с использованием useDispatch
useDispatch()
это способ, с помощью которого компоненты отправляют действия.
3. Оберните приложение в Provider
<Provider store={store}>
4. Помните правила сравнения
useSelector → ===
connect → shallow comparison
5. Никогда не выбирайте корневое состояние
useSelector(state => state)
6. Будьте осторожны с выбрасывающими объекты функциями выбора
useSelector(state => ({
...
}))
7. Хранение в кэше дорогостоящих вычисляемых данных
Memoized selector
8. Соблюдение правил формирования селекторов
Pure
Predictable
Granular
9. Сначала определите типы хранилища, затем хуки
Сначала автоматически определенные типы:
RootState
AppDispatch
AppStore
затем хуки приложения, созданные на их основе:
useAppSelector
useAppDispatch
useAppStore
10. Используйте современные практики парного использования
Redux Toolkit
+
React-Redux Hooks
Вся API на одной странице
Компактный справочник, охватывающий основные хуки, рекомендации по производительности, типы TypeScript, устаревшую API и продвинутые темы:
┌───────────────────────────────────────────────┐
│ REACT-REDUX │
├───────────────────────────────────────────────┤
│ │
│ <Provider store={store}> │
│ ↓ │
│ Makes Redux available to React │
│ │
│ useSelector() │
│ ↓ │
│ READ state │
│ ↓ │
│ Default comparison: === │
│ │
│ useDispatch() │
│ ↓ │
│ DISPATCH actions │
│ │
│ useStore() │
│ ↓ │
│ Direct store access │
│ ↓ │
│ Use rarely │
│ │
├───────────────────────────────────────────────┤
│ PERFORMANCE │
├───────────────────────────────────────────────┤
│ │
│ Avoid state => state │
│ Avoid unnecessary object creation │
│ Use granular selectors │
│ Use shallowEqual when appropriate │
│ Use memoized selectors for derived data │
│ Use React.memo when justified │
│ │
├───────────────────────────────────────────────┤
│ TYPESCRIPT │
├───────────────────────────────────────────────┤
│ │
│ RootState = ReturnType<typeof store.getState> │
│ AppDispatch = typeof store.dispatch │
│ AppStore = typeof store │
│ │
│ useAppSelector │
│ useAppDispatch │
│ useAppStore │
│ │
├───────────────────────────────────────────────┤
│ LEGACY / EXISTING APPLICATIONS │
├───────────────────────────────────────────────┤
│ │
│ connect() │
│ mapStateToProps │
│ mapDispatchToProps │
│ ConnectedProps │
│ │
├───────────────────────────────────────────────┤
│ ADVANCED │
├───────────────────────────────────────────────┤
│ │
│ Stale Props │
│ Zombie Children │
│ Custom Context │
│ SSR serverState │
│ Development checks │
│ batch() │
│ │
└───────────────────────────────────────────────┘
А также цикл, изображенный снова с путями чтения и записи рядом друг с другом:
REACT
│
│
<Provider />
│
▼
┌─────────────┐
│ Redux Store │
└──────┬──────┘
│
┌───────────┴───────────┐
│ │
▼ ▲
useSelector() useDispatch()
│ │
│ │
READ ACTION
│ │
│ │
│ ┌────┴─────┐
│ │ Reducer │
│ └────┬─────┘
│ │
│ ▼
│ New State
│ │
└───────────────────────┘
│
▼
Equality Check
│
┌──────┴──────┐
│ │
Same Changed
│ │
▼ ▼
No Redux Re-render
render
Для нового проекта на TypeScript рекомендуемая конфигурация выглядит примерно так:
Redux Toolkit
+
React-Redux
│
┌──────────┴──────────┐
│ │
Store Provider
│ │
│ Application tree
│ │
└──────────┬──────────┘
│
┌────────┴────────┐
│ │
useAppSelector() useAppDispatch()
│ │
READ WRITE
│ │
└────────┬────────┘
│
Redux
│
New State
│
Selector
│
Re-render
Заключение
React-Redux становится гораздо проще в использовании, когда вы перестаете рассматривать его экспорты как не связанные между собой инструменты. Этот список названий
Provider
useSelector
useDispatch
connect
shallowEqual
createSelector
useStore
Описывает одну пайплайн-структуру с одной задачей на каждом этапе. Провайдер предоставляет хранилище данных:
Provider
↓
makes Store available
Хук выбора считывает данные:
useSelector
↓
reads selected state
Хук диспетчеризации отправляет команды:
useDispatch
↓
sends actions
Редьюсеры генерируют следующее состояние:
Reducer
↓
creates new state
Селекторы формируют состояние для отображения в интерфейсе:
Selector
↓
derives data
Проверка на равенство определяет, изменились ли какие-либо важные данные:
Equality
↓
decides whether selected data changed
А React отвечает за отрисовку:
React
↓
re-renders when necessary
В TypeScript цепочка операций также имеет линейную структуру:
Store
↓
RootState
AppDispatch
AppStore
↓
.withTypes()
↓
useAppSelector()
useAppDispatch()
useAppStore()
Когда компонент отрисовывает больше данных, чем следует, необходимо каждый раз рассматривать те же четыре вопроса:
useSelector()
↓
What does my selector return?
↓
Is the reference stable?
↓
Does the selected value actually change?
↓
Should this component re-render?
Основные выводы:
- Большинство проблем с производительностью в React-Redux связаны со селекторами, которые возвращают новые ссылки, а не с самим Redux.
useSelector() сравнивает значения с помощью оператора ===; connect() выполняет поверхностное сравнение. Перенос кода между ними без корректировки селекторов приводит к изменению поведения.createSelector на уровне модуля, и применяйте shallowEqual только тогда, когда необходим результат в виде объекта.RootState, AppDispatch и AppStore на основе хранилища данных и создавайте типизированные хуки с использованием метода .withTypes().batch() и параметр serverState, важно, но это крайние случаи, а не повседневные проблемы.В качестве самопроверки попробуйте объяснить из памяти каждый раздел этого руководства — от Provider и принципа равенства, через типизированные хуки до serverState — и написать базовую настройку, не обращаясь ни к чему в поиске.
Для получения подробной информации официальными источниками являются API хуков, API Provider, Руководство по использованию с TypeScript, Быстрое начало и Документация к функции connect(). Сначала изучите основной цикл и принцип равенства, затем селекторы, TypeScript, вопросы производительности, connect и крайние случаи, чтобы у вас была основа для понимания деталей API.
Связанная литература
- React Query и Redux: переосмысление состояния сервера в крупных приложениях — Узнайте, почему в производственном чат-приложении использовался TanStack Query вместо Redux для управления серверными данными, и где Redux всё ещё находит своё место в современной архитектуре React.
- Структурирование слоя данных TanStack Query: от queryOptions до функций возврата к предыдущему состоянию — Постепенно создайте слой данных TanStack Query: общие параметры запросов, генераторы ключей, селекторы, пагинация, предзагрузка данных, централизованная аннуляция запросов и безопасные оптимистичные обновления.