Головна / Статті / Як React-Redux вирішує про перерендеринг: селектори, принципи порівняння та типовані хуки

Як React-Redux вирішує про перерендеринг: селектори, принципи порівняння та типовані хуки

Посібник з вивчення внутрішньої роботи React-Redux: Provider, принципи порівняння з useSelector, мемоїзовані селектори, функція connect(), типовані хуки з withTypes() та рідкісні крайні випадки.

7738 слів

Здається, що фреймворк 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 виконує невелику процедуру:

  1. читає поточний стан сховища
  2. викликає ваш селектор з цим даними
  3. зберігає повернене значення як результат „останнього вибору“
  4. реєструє підписку на сховище для цього компонента
  5. знову виконує селектор після кожної дії, яка була надіслана
  6. порівнює попередній результат із новим
  7. планує перерендеринг лише тоді, коли порівняння показує різницю

Саме на шостому кроці виникає майже все несподіване поведінку.

Стандартне порівняння — це сувора рівність посилань

За замовчуванням хук

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

Ваше завдання — написати селектори, які:

  1. повертають лише ті дані, які використовує компонент
  2. повертають стабільні посилання, якщо нічого значущого не змінилося
  3. не створюють нових об’єктів чи масивів без необхідності
  4. мемоюють похідні дані, обчислення яких є складним

Перше правило: ніколи не обирати кореневий стан

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

Послідовність дій виглядає так:

  1. Дочірній елемент підписується на дані з магазину даних.
  2. Якась дія видаляє дані, які відображає дочірній елемент.
  3. Під час наступного оновлення батьківський елемент припинить відображати цей дочірній елемент.
  4. Перш ніж це станеться, виконується обробка підписки дочірнього елемента.
  5. Селектор дочірнього елемента намагається отримати дані, яких вже немає.

На останньому кроці виникає помилка через незахищений селектор. 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 виконується так часто?

Він:

  1. виконується під час відображення
  2. підслуховує зміни в 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()
    • давно застарілі патерни
    • деталі реалізації вихідного коду
  • точний текст кожного попередження про розвиток
  • кожен складний проп Provider
  • Важливим є обґрунтування правил. Знання цього факту

    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 була модель для аналізу.

    Пов’язана література

  • Prop Drilling — це не причина для встановлення Redux чи Zustand — Протестуємо чотири поширені причини додавання бібліотеки для керування станом у функціональному коді React: використання Context для передачі значень через пропи, useState, useSyncExternalStore та витрати на перерендеринг через Context.
  • Скейлер React та цикл подій: хто насправді вирішує, коли виконуватиметься робота — Дізнайтеся, як кооперативний скейлер React працює всередині циклу подій JavaScript, чому можуть відбуватися переходи між станами та чому жоден скейлер не може врятувати заблоковану нитку.
  • Міркування щодо React Hooks через знімки відображення та компроміси — рівень старшого фахівця щодо моделей мислення для React та React Native Hooks: знімки відображення, ефекти, refs, мемоїзація, власні Hooks та способи пояснення компромісів під час співбесід.