Галоўная / Артыкулы / Як 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 атрабатвае UI. 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: як зробіць хранілню доступной

Hooks можаўць вяснавацца толькі з хранілнёй, якая знаходзіцца вышэй у іерархіі. <Provider> якраз і ставяе ёю туды. Зазвычай корэнь аплікацыі обвязваецца яго ўсьмоўна:

import { Provider } from 'react-redux'
import { store } from './store'
function AppRoot() {
  return (
    <Provider store={store}>
      <App />
    </Provider>
  )
}

Па суті, Provider размешчае хранілню ў контэксте React. Кожны елемент-наследник, незалежна ад глыбіні, можа да яе дасягнуць без неабходнасці праходжэння праз роўныя элементы іерархіі:

<Provider store={store}>
       │
       ├── App
       │    ├── Header
       │    ├── Dashboard
       │    ├── UserProfile
       │    └── Settings
       │
       └── Every descendant
            can use Redux

Якщо компонент вызывае адзін з хуков за межамі Provider, то няма сховішча, з якога можна было б чытаць данні, і хук не функцыонуе (на практыцы ён выклікае адказ пра тое, што не хватает значэння контексту):

useSelector(...)
useDispatch(...)

Часта прычына такой проблемы — у тэстах на едыніцы, дзе компонент атрыбутуецца без паказвання яго ў тэстовым Provider.

useSelector: як компоненты чытаюць стан

useSelector() — гэты хук, які вы будете вызываць найчыльней. Вы даёте яму функцыю, якая прыймае весь стан Redux і вяртае ту частку, якая патрабуецца компонентам:

const count = useSelector(
  state => state.counter.value
)

Форма потоку даных простая:

Redux State
     │
     ▼
useSelector()
     │
     ▼
Selected Value
     │
     ▼
React Component

Пакалькі React-Redux можа вызываць ваш выбірач чырэз больш часу, ніж вы спадзяваліся (пад час атрыбутавання, пасля выклікання dispatch-аў, пад час перагляду ў режыме разработкі), таму выбірач павінен быць чыстым: тыя ж уводныя данні, тыя ж выходныя данні, без парадоксальных наследків.

Што робіць хук пад кожным атрыбутаванням і выклікам dispatch-а

Взяце селектар, які выбірае тэчны пользователя:

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

Стабільныя калебаксы для мемаваных дзецэў

Большасць калебаксаў dispatch не патрабуюць 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 вышэйшага порядку для компанентав

Hooks ўважаюцца рэкамендаваным спосабам, але connect() так і застаўся у вжыцці, і багато давгастоячых кодавых баз пабудованы на ям. Можна спакуяцца зустрэць такі код:

connect(
  mapStateToProps,
  mapDispatchToProps
)(Component)

Як connect перадае даны з store у props

Ментальная модэль — это обертка, якая ператварае даны з store і функціі dispatch у звычныя props:

Redux Store
     │
     ▼
connect()
     │
     ├── mapStateToProps
     │
     └── mapDispatchToProps
     │
     ▼
Component Props

mapStateToProps прымеў стан і вяртае об’ект з props:

const mapStateToProps = state => ({
  user: state.auth.user,
  count: state.counter.value
})

Па суті ён выканае такую перакладчыку:

Redux State
     ↓
Component Props

Обернуты компанент застаецца звычной функцыяй своіх 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)

А калі вжоўваюцца оба аргументы, яна чытае данні і выкананяе дзеяння dispatch:

connect(
  mapStateToProps,
  mapDispatchToProps
)(Component)

Знаёмасць таго, што форма з null прыменшвае колькасць падпісаў, ёсць корыстна: гэта дашчавы спосаб прызначыць компанатце права на dispatch без таго, каб яна рэндарылася праз апдаты стоара.

connect вяртае новую компанатцу

Выклік

connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent)

не зменяе MyComponent. Ён стварае окраняючую компанатцу, якая рэндарые вашу компанатцу ўсередзіне сябе:

MyComponent
     │
     ▼
connect()
     │
     ▼
ConnectedComponent

Самэй таго модулі, якія выкарыстоўваюць connect, зазвычай экспортуяць такую окраняючую версію як стандартную, а іноды окрема экспортуяць чыстую компанатцу для тэстаў.

Выбір между 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: тип дыспача, ўкладаючы мідлвэр

Так сама вывучайце тип дыспача:

export type AppDispatch =
  typeof store.dispatch

Звычны тип Redux Dispatch ведае толькі звычныя акцыі; вывучаны тип адбівае ваш мідлвэр, што мае значэнне, калі вы викорыстоўваеце:

  • мідлвэр, які зменяе тое, што прыймае dispatch
  • thunks
  • індывідуальны дыспач
  • асынхронныя акцыі любага типу

Без AppDispatch дыспач тханка ўзводзіць да памылкі типу.

AppStore: тип самага магазіна

Тып магазіна вывучаецца ўсьмо ж такім спосабам:

export type AppStore =
  typeof store

Такім чынам, кожны аспект мае ўнікальны источнік правды. Тып стану:

RootState
    ↓
state type

Тып дыспатча:

AppDispatch
    ↓
dispatch type

Тып хранення:

AppStore
    ↓
store type

AppStore становіцца особліва корыстным, калі вы ствараеце хранення па кожнай запытке або па кожным тэсту і трэба яго перадаваць.

Заздалегідь заданыя хукі з withTypes()

Пачынаючы з React-Redux 9.1.0, кожны хук аб’являе метод .withTypes():

useDispatch.withTypes()
useSelector.withTypes()
useStore.withTypes()

Документаваныя шаблоны дазволяюць ствараць хукі, прызначаныя для конкретных застосоўваń, з іх аднойчы:

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

Што дапамагаюць типаваныя хукі

Без іх кожны селектар патрабуе явнай анотацыі:

const user = useSelector(
  (state: RootState) =>
    state.auth.user
)

За дапамогою типаванага хука,

const user = useAppSelector(
  state => state.auth.user
)

кампайляр вже знае

state = RootState

Частка дыспатча працюе так сама:

const dispatch = useAppDispatch()

Этый dispatch прымае функцыі-thunk і ўсё іншае, што дазволяе ваш мідлвэр, з абовесным пераканальнем.

Файл hooks.ts для аплікацыі

У типовых проектах гэтыя элементы зберагаюцца ў аднам маленькім модуле:

import {
  useDispatch,
  useSelector,
  useStore
} from 'react-redux'

import type {
  RootState,
  AppDispatch,
  AppStore
} from './store'

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

Компаненты імпортуюць матэрыял з гэтага модуля, а не безпасебна з react-redux:

const user = useAppSelector(
  state => state.auth.user
)

const dispatch = useAppDispatch()

ConnectedProps для коду connect() з типамі

Кодавыя базы, у якіх компаненты connect() маюць типаванне, будуць содержаць

ConnectedProps

Раздзеліце вызов connect на спачатку стварэнне канектара, а пасля — выяўленне пропсаў, якіе ён вводзіць:

const connector = connect(
  mapState,
  mapDispatch
)

type PropsFromRedux =
  ConnectedProps<typeof connector>

PropsFromRedux точна описвае, што вводзіць канектар, таму мапаваныя типы ніколі не падражнаюцца.

Проектаванне хорашых селектараў

Існуе спакуса супакоўвацца, што селектар — гэта проста

state => state.user

У большых прыграмах селектары лепша спрыяюць як межа, яка ператварае формат зберагчыка стану ў той фармат, які патрабуе інтэрфейс:

Redux State
     ↓
Selector
     ↓
UI-friendly data

Напрыклад, правіла, якія выбіраюць, калі паказваць элементы, могу знаходзіцца ў адной названай функцыі:

const selectVisibleTodos =
  state =>
    state.todos.filter(
      todo => !todo.hidden
    )

а компонент проста запрашвае рэзультат:

const todos = useSelector(
  selectVisibleTodos
)

Компонент застаецца прымітным для паказу, а правіла можна тэставаць окрема.

Хорашы селектар — гэта тачны

const selectUserName =
  state => state.auth.user.name

Ён вяртае самэ тое, што паказвае компонент, і нічога больш, таму ён запускае перзапісь толькі тады, калі зменяецца гэта назва.

Выбіранне всего стану практычна завжды ўскладненне

const selectEverything =
  state => state

Redux стварае новы об’ект корневаго стану кожны раз, калі які-небудзь редюсер зменяе ўсё. Селектар, який вяртае корневы стан, таму вяртае новую рэферэнцю практычна пасля кожной дзеяння:

Anything in Redux changes
        ↓
Root state reference changes
        ↓
Selector result changes
        ↓
Component re-renders

Прынцыпы разработкі React-Redux выяўляюць такі патэрн.

Зберагаюце селектары ў дробным формате

Вядзьміце прыоритет колькам цялевым запытам:

const count =
  useSelector(
    state => state.counter.value
  )

const user =
  useSelector(
    state => state.auth.currentUser
  )

замест адного запыту, які працуе з усім:

const state =
  useSelector(state => state)

Практычная правіла:

Выберыце самую маленькую частку стану, яюя компонент можа фактычна выкарыстоўваць.

Прынцыпы перагляду селектараў у режыме разработкі

Няўясковыя версіі React-Redux адбываюць дадатковыя перагляды вашых селектараў у режыме разработкі. Два з іх варта памяць па назве.

Прынцып пераканальнасці

Першы перагляд вызывае ваш селектар ўтроці з тым самым станам і паўставляе рэзультаты:

selector(state)
      ↓
run again with same state
      ↓
same result?

Якшо рэзультат

same reference

то селектар є пераканальным. Якшо ж ні

new reference

React-Redux выдае паведамленне пра адвары, таму што селектар, які вяртае новы кансэльт для ідэнтычных даных, будзе перерабляць свой компонент пасля кожнай змены стоу.

Тыповы прыклад — селектар у вигляде об’екта, показаны раней:

const data = useSelector(
  state => ({
    count: state.count,
    user: state.user
  })
)

Паколькі об’ект перабудовваецца пасля кожнага вызову, пераканаленне бачыць:

same input
    ↓
different object
    ↓
unstable selector

Настройка частоты выканання пераканаленняў

Вы можете задаць частоту для всіх прыкладоў аплікацыі ў элементе Provider:

<Provider
  store={store}
  stabilityCheck="always"
>
  <App />
</Provider>

альбо перакрыць яе для конкрэтнага вызову хукі:

const count = useSelector(
  selectCount,
  {
    devModeChecks: {
      stabilityCheck: 'once'
    }
  }
)

Дазволеныя значэння ўключаюць:

never
once
always

Стандартнае значэнне — 'once', што означае, што пераканаленне выкананае пасля першага вызову кожнага хукі. Нічыя з гэтых функцый не працюе ў версіях аплікацыі для рэальнай експлуатацыі.

Пераканаленне на ідэнтычнасць функцыі

Другое пераканаленне шукае селектар, які вяртае свае даны без змян:

state => state

У компаненте это выглядае так:

const state = useSelector(
  state => state
)

Это запоўнюе сувязь між компанентам і кожной змянай у стое.

Any Redux change
       ↓
Root state changes
       ↓
Component re-renders

У дасведчэннях гэта называецца перакананнем функцыі ідэнтычнасці. У болей раніх версіях гэта называлася noopCheck, і вы можаце ўсё яшчэ пабачыць гэты назваў у старых настройках.

Рашэнне такое ж, як і ранейш: заменіце

const state = useSelector(
  state => state
)

на чытанне конкрэтных значэнняў, якія вам патрабуюцца:

const count = useSelector(
  state => state.counter.value
)

const user = useSelector(
  state => state.auth.currentUser
)

Атрыбутаванне і выдатнасц за межамі селектараў

Атрыбутаванне родніка все ўсё каскадуецца

useSelector() керуе толькі атрыбутаванням, якое выкалічваецца пасля змян у стое. Ён нічога не робіць з стандартным правілам React, па якому компанент атрыбутаваецца, калі атрыбутуецца яго роднік:

Parent renders
      ↓
Child renders

Гэта выкалічваецца нават тады, калі стан Redux ужо не змяніўся. Калі дзеціны компаненты є ресурсоўна выкалічваючымі, а ўсе іх пропсы застойныя, пакруціце іх у

React.memo()

Гэта не аднакова з connect(), чыя абляцыйна функцыя паводзіцца як компонент з мемаўваннем; компоненты, базаваныя на хукіх, не атрымваюць такога якостку без дадатковых зусібоў.

Спалучэнне React.memo з useSelector

У гэтым прыкладзе компонент падпісваецца на лямбда-функцыю, якая вылічае лічыльнік, і таксама мемаўваны за сяродністю з яго атрыбутам name:

const Counter = ({ name }) => {
  const count = useSelector(
    state => state.counter.value
  )

return (
    <div>
      {name}: {count}
    </div>
  )
}
export default React.memo(Counter)

Гэтыя два механізмы пакрываюць два выклікі рэндарування:

Redux selector
       +
React.memo
       ↓
More controlled rendering

Мемаўванне мае свою цэну, таму спачатку аналізуйце ўражанне на выконвальную скорасць. Чынны каталог фактараў, якія спрычыняюць рэндарування, можна пазначыць за лінкам нашым кялікам пра распашчастыя патэрны, якія спрычыняюць непатрэбныя перарэндаруванняў у React.

Модэль выконвальной скорасці, якая падходзіць на аднай экране

Пасля кожнай дзеяння React-Redux зноў запуска селектары падпісаных компонентаў, поручвае кожны рэзультат з пярэднім і адрасавае на екран толькі тыя компоненты, чыя рэзультаты разніцяюцца:

Redux action
     ↓
Store updates
     ↓
Selectors execute
     ↓
Selector results compared
     ↓
Changed?
 ┌───┴────┐
No       Yes
│         │
│         ▼
│      Re-render
│
└── No Redux-triggered render

Ваша задача — напісаць селектары, якія:

  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

У старэйшым коде часта выкорыстоўваецца batch() для абгорткі паўзловых вызываў dispatch, каб React адрасаваў раз уместо двух разоў:

batch(() => {
  dispatch(action1())
  dispatch(action2())
})

У React 18 апдэты групаваныя автаматычна, укладаючы ў гэтае групаванне нават обявы з promises і таймаутамі, таму стандартны проект на 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, часта арганізуецца па функцыях, пры чым канектаванне з хранальнікам данных вырабляецца ў адным месцы:

src/
│
├── app/
│   ├── store.ts
│   └── hooks.ts
│
├── features/
│   │
│   ├── counter/
│   │   ├── counterSlice.ts
│   │   └── Counter.tsx
│   │
│   ├── users/
│   │   ├── usersSlice.ts
│   │   └── Users.tsx
│   │
│   └── auth/
│       ├── authSlice.ts
│       └── Login.tsx
│
├── App.tsx
└── main.tsx

store.ts

Модуль хранальніка данных настаўляе редукеры і вывозіць вярнутыя типы:

const store = configureStore({
  reducer: {
    counter: counterReducer,
    users: usersReducer,
    auth: authReducer
  }
})

export type RootState =
  ReturnType<typeof store.getState>

export type AppDispatch =
  typeof store.dispatch

export type AppStore =
  typeof store

hooks.ts

Модуль хукіў ператварае гэтыя типы на хуки аплікацыі:

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

Компонент, які викорыстоўвае і тое, і другое

Компонент-лічыльнік чытае і запісвае данні толькі чераз типаваныя хуки:

function Counter() {
  const count = useAppSelector(
    state => state.counter.value
  )

const dispatch = useAppDispatch()
  return (
    <>
      <span>{count}</span>
      <button
        onClick={() =>
          dispatch(increment())
        }
      >
        +
      </button>
    </>
  )
}

Працэс выконання ў гэтым компоненте адпавядае самэй основнай петлі з самага пачатку:

Component
    │
    ├── useAppSelector()
    │        ↓
    │      READ
    │
    └── useAppDispatch()
             ↓
          DISPATCH
             ↓
           Redux
             ↓
          New State
             ↓
       useAppSelector()
             ↓
          Component

Дзе паслужыць Redux Toolkit

Redux Toolkit і React-Redux ўзаімадопамагаюць, а не являюцца альтэрнатывамі:

Redux Toolkit
      +
React-Redux

Redux Toolkit павышае эфектыўнасць часткі Redux: настройка хранення дадзенаў, редукеры, асінхронная логіка, мемаваныя селектары і запошук дадзенаў.

configureStore
createSlice
createAsyncThunk
createSelector
RTK Query

React-Redux продавае адпаведнасць за з’яўленне ў React:

Provider
useSelector
useDispatch
useStore
connect

Ofіцыйны швыдкі стартап з 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'

наражае на парадокс у модэлі незмennyх апдэйтаў Redux: рэферэнсы не зміняюцца, таму селектары бачаць „жадных змян“ і компоненты не рэндаруюцца. (У редакторах createSlice з Redux Toolkit такі падход дазволены, таму што Immer ператварае яго на незмennyю зміну; па-іншаму ўсюды гэта являе сабою баг).

Побачныя эфекты ўнутрь селектараў

state => {
  fetch(...)
  return state.user
}

Селектар должен выраховваць і вяртаць, нічога больш.

Мемавацыя па рэфлексе

Распалюванне іх па всіх месцах

useMemo()
useCallback()
React.memo()

Гэта дадзеяе сложнасць і додатковы навантажэння на пораўнанні без падтверджэння, што гэта дапамагае. Оптымізавайце тое, што вже змерылі.

Неправильная разметка экзанпляраў мемаваных селектараў

Селектар з кэшам працуе інако залежна ад свайго дыяпазону. Перш чым яго вы вжываеце, з’ясавайце, чы ён:

global
per component
per component instance

Екзанпляр, які дзеліцца і выкарыстоўваецца з многама разнымі аргументамі, можа ніколі не выкарыстоўваць свой кэш.

Запытанні пад час адбору на работу з короткімі адпаведзеннямі

Што такое React-Redux і чаму патрэбны Provider?

Это офіцыйная прыемка React для Redux: Provider, хукі і connect дазволяюць компонентам чытаць стан, реагаваць на змяны і адправляць дзеяння. Provider размешчае хранілню ў контэксте React, так што будзь-які наследнік можа да яе дасягнуць.

useSelector проты useDispatch?

Одны выкарыстоўваецца для чытання:

useSelector
    ↓
READ Redux state

Іншы варыянт:

useDispatch
    ↓
DISPATCH Redux actions

Як useSelector спрыяе паўтарнаму атрыбутаванню?

Пасля кожной дзеяння ён зноў запускае селектар і порынае рэзультат з пярэднім за дапамою === або функцыі равенства, якую вы прасілі. Толькі разліка запускае процес атрыбутавання.

Чаму гэты селектар постаўна спрыяе паўтарнаму атрыбутаванню, і як гэта выправіць?

useSelector(state => ({
  user: state.user,
  count: state.count
}))

У кожны раз, калі ён запускаецца, ствараецца новы об’ект, таму перакананне ў аднаковасці рэферэнаў завжды не выходзіць. Тры спосабы рашэння:

1. Multiple useSelector calls

2. shallowEqual

3. Memoized selector

useSelector проты connect?

Хукі порынаюць рэзультаты селектара па рэферэнах; connect() адбывае паверхневыя порэванні пропсаў з mapStateToProps. Хукі є стандартам, а connect() застаецца падтрымліваным.

Чаму патрэбны мемаізаваныя селектары?

Яны перасчыляюць вылучаныя даны толькі тады, калі зменяюцца вхідныя даны, а інакш чыраюць тэты ж рэферэнс. Класычным прыкладам ёсть фільтры:

todos
  ↓
filter completed
  ↓
new array

Што такое RootState, AppDispatch і withTypes()?

Вылучаны тип усьго стану:

type RootState =
  ReturnType<typeof store.getState>

Вылучаны тип дыспача, уключаючы мідлвэркі, такія як thunks:

type AppDispatch =
  typeof store.dispatch

І дапаможнікі, якія чыраюць хукі, прадзеактываўаныя для гэтых типаў:

useDispatch.withTypes<AppDispatch>()

useSelector.withTypes<RootState>()

useStore.withTypes<AppStore>()

Чаму хукі з типамі?

Яны заменяюць павтаральныя анотацыі, такія як

useSelector(
  (state: RootState) =>
    state.user
)

на

useAppSelector(
  state => state.user
)

Чаму селектары павінны быць чыстымі?

Селектар можа выконвацца колькаразова для таго ж стану пад час атрыбутавання, пасля кожнай дзеянні і ў моменты, якія ваш код не кантролюе. Будзь-які парадоксальны эфект выконвацца будзе непраўдападобным колькісцю разоў.

Для чаго патрэбна функция useStore?

Рэдкія ситуацыі, калі дасправда патрэбны об’ект магазіна. Чытанне стану для адраджавання ведаецца через useSelector().

Што такое „зомбі-дзеця“ і застарэлыя пропсы?

Абодва ўскладнення являюцься рэдкімі проблемамі з арранжаваннем. „Зомбі-дзеця“ обробляюць апдэйты прытаманнія свайго клёна, пакуль ўсё ще не адключаныя з бацэння клёна-родзіча, і чытаюць выдаленыя данні; застарэлыя пропсы означаюць, што селектар, які залежыць ад пропсаў, запускаецца з нэвыканутым станам, але з старымі пропсамі. Захоўнічныя селектары дапамагаюць рашыць абодве проблемы.

Чы роўна ў React 18 яшчэ патрэбны функцыі batch()?

Зазвычай няма, калі толькі React 18 автаматычна аб’еднвае операцыі, але ў старэйшым кодзе яны все ж можаць зустріцца.

Чаму connect і хукі можаюць адраджаваць розна?

Разныя модэлі падпіску і разныя способы пораўнення:

connect()
    ↓
shallow equality

протыва

useSelector()
    ↓
strict === equality

Чаму useSelector запускаецца так часта?

Ён:

  1. запускаецца пад час адраджавання
  2. слухае за змянамі в магазіне
  • Паўтарна выконанне пасля кожнай адправленай дзеяння
  • Паўторна выкарыстоўванне рэзультата з кеша пад час адрасавання толькі тады, калі функцыя селектара і стан не зменіліся
  • Такім чынам, інлайн-селектар, які ў кожны раз пад час адрасавання є новай функцыяй, выконваецца ў кожны раз; стабільная рэферэнцыя селектара дазволяе 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
    

    Селектары формуюць яго для UI:

    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» і равенства праз «typed hooks» да serverState, і напісаць базавы настаўленні, не шукаючы нічога.

    Для дакладнасці офіцыйныя інструкцыі — это hooks API, Provider API, інструкцыя па викорыстоўванні з TypeScript, Quick Start і інструкцыя да connect(). Спачатку вучыцеся аб основнам цыкле і равенства, пасля — аб селектарах, TypeScript, выконванні, connect і краявых случаях, ўсё гэта да таго, каб дакладнасці API мелі модэль для прыўязкі.

    Супаўзвяжаная літэратура

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