Як 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: забезпечення доступу до store
Hooks можуть взаємодіяти лише з store, який знаходиться вище них у ієрархії. <Provider> саме те, що розміщує його там. Зазвичай корень додатку обгортається один раз:
import { Provider } from 'react-redux'
import { store } from './store'
function AppRoot() {
return (
<Provider store={store}>
<App />
</Provider>
)
}
Під капотом Provider поміщає store у контекст 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 виконує невелику процедуру:
- читає поточний стан сховища
- викликає ваш селектор з цим даними
- зберігає повернене значення як результат „останнього вибору“
- реєструє підписку на сховище для цього компонента
- знову виконує селектор після кожної дії, яка була надіслана
- порівнює попередній результат із новим
- планує перерендеринг лише тоді, коли порівняння показує різницю
Саме на шостому кроці виникає майже все несподіване поведінку.
Стандартне порівняння — це сувора рівність посилань
За замовчуванням хук
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
референції залишаються ідентичними, тож перевірка === проходить успішно.
Немає штрафу за кілька викликів хука в одному компоненті. Якщо один диспач змінює більше ніж одне з обраних значень, React-Redux групує отримані оновлення, тож компонент все одно відображається лише один раз після цього диспачу, а не по одному разу за кожен хук.
Виправлення два: 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
Створення selector за допомогою 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 прив’язує кожну функцію створення дій так, що її виклик через пропс автоматично її відправляє. Це зазвичай більш охайний варіант.
Чотири поширені формати connect
Ви зустрінете чотири варіації. Без аргументів компонент отримує лише dispatch як пропс:
connect()(Component)
Якщо є лише мапер стану, компонент читає дані, але не отримує прив’язаних функцій створення дій:
connect(
mapStateToProps
)(Component)
Якщо першим аргументом є null, компонент ніколи не прослуховує стор, а отримує лише властивості dispatch:
connect(
null,
mapDispatchToProps
)(Component)
А якщо обидва аргументи вказані, він читає дані та відправляє їх:
connect(
mapStateToProps,
mapDispatchToProps
)(Component)
Важливо знати, що форма з null пропускає підписку — це дешевий спосіб надати компоненту доступ до dispatch без необхідності його оновлення під час змін у сторі.
connect повертає новий компонент
Виклик
connect(
mapStateToProps,
mapDispatchToProps
)(MyComponent)
не змінює MyComponent. Він створює окремий обгортковий компонент, у якому відображається ваш компонент:
MyComponent
│
▼
connect()
│
▼
ConnectedComponent
Саме тому модулі, підключені до стору, зазвичай експортують обгорнуту версію як стандартну, а іноді окремо експортують простий компонент для тестування.
Вибір між hooks та 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: тип dispatch з урахуванням мідлверу
Визначайте тип dispatch таким самим способом:
export type AppDispatch =
typeof store.dispatch
Звичайний тип Redux Dispatch відомий лише звичайним діям; отриманий тип враховує ваш мідлвер, що є важливим, якщо ви використовуєте:
- мідлвери, які змінюють те, що приймає
dispatch - thunks
- індивідуалізований dispatch
- асинхронні дії будь-якого типу
Без AppDispatch використання thunk призводить до помилки типу.
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
)
Це пов’язує компонент із кожною зміною у store:
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() керує лише відображенням, спричиненим оновленнями store. Він не впливає на звичайне правило 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 для SSR та гідратації
Для серверного рендерингу компонент Provider отримує додаткове властивість:
<Provider
store={store}
serverState={preloadedState}
>
Сервер генерує HTML з початкового стану та надсилає цей стан браузеру. serverState забезпечує використання того самого знімка під час процесу гідратації, що запобігає неузгодженостям:
Server
↓
Initial Redux State
↓
HTML
↓
Browser Hydration
↓
Provider(serverState)
↓
Consistent initial render
Типова структура проекту
Застосунок на TypeScript, побудований за допомогою Redux Toolkit, зазвичай організований за функціями, причому підключення до store знаходиться в одному місці:
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
Модуль store налаштовує редюсери та експортує виведені типи:
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
Модуль hooks перетворює ці типи на хуки застосунку:
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?
Рідкісні випадки, коли справді потрібен об’єкт store. Читання стану для відображення відбувається через useSelector().
Що таке „зомбі-діти“ та застарілі пропси?
Обидва це рідкісні проблеми з порядком виконання. „Зомбі-дитина“ обробляє оновлення до того, як її батько її видаляє, і читає видалені дані; застарілі пропси означають, що селектор, який залежить від пропсів, виконується зі свіжим станом, але зі старими пропсами. Захисні селектори допомагають вирішити обидві проблеми.
Чи все ще потрібен batch() у React 18?
Зазвичай ні, оскільки React 18 автоматично об’єднує операції, але ви все ще можете зустріти його у старішому коді.
Чому connect та хуки можуть відображати різні результати?
Різні моделі підписки та різні способи порівняння:
connect()
↓
shallow equality
проти
useSelector()
↓
strict === equality
Чому useSelector виконується так часто?
Він:
- виконується під час відображення
- підслуховує зміни в store
Отже, інлайн-селектор, який є новою функцією при кожному відображенні, виконується при кожному з них; стабільний посилання на селектор дозволяє 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 до Rollbacks — Поступово створіть шар даних TanStack Query: спільні queryOptions, генератори ключів, селектори, пагінація, попереднє завантаження, централізоване скасування та безпечні оптимістичні оновлення.