Як React-Redux выбірае, калі перераскладаць элементы: селектары, способнасць выявлення равенства і хукі з падтрымкам типаў.
Наводчык па внутрэшняй частцы React-Redux: Provider, способнасць useSelector да выявлення равенства, мемузаваныя селектары, функцыя connect(), хукі з падтрымкам типаў за дапамою withTypes(), а таксама рэдкія крайнія прыклады.
Спектар выкарыстоўвання React-Redux здаецца мінімальным: компонент provider і калякве хукіў. Аднак у большых кодавых базах тыя ж пытанні продовжваюць з’являцца. Чаму гэты компонент атрыбутуецца праз кожны dispatch? Чаму селектар, які вяртае об’ект, паводзіцца інакше, чым mapStateToProps? Калі shallowEqual ёсць правым інструментам, што на самай працэ RootState і AppDispatch насправды описваюць, і чы роўна batch() яшчэ неабходны ў React 18?
У гэтым кяліку даўаюцся адпаведзі на гэтыя пытанні, следуючы адной лініі разумовага працэ: што вядзецца межы атрыбутаваннем дзеяння і паўторным атрыбутуваннем. Калі гэты пацек стане ясным, адзінкавыя API перестаюць быць спісам, які трэба запам’ятаваць, і становяцца часткамі адной системы, пра яку можна рассуждаць, дэбагаваць і адказваць пад час перагляду коду чы інтэрв’ю.
useSelector()
useDispatch()
<Provider />
Чырвоныя рэкамендаціі: адміністраторы React-Redux рэкамендуюць API Hooks як стандарт для компанентаў.
connect()все ўсё падтрымліваецца і заўсёды важна знать, таму што багато існуючых кодаў апыяецца на яму.
Што такое React-Redux і якое ў яго ролі
Redux адрабатвае стан, запускае редукцэры і павядамляе слухачаў; React атрабатвае 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 выкананая невеликая процедура:
- чытае тэчны стан хранілніка
- выклекае ваш селектар з гэтым дадзеннем
- зберагае повернутая значэнне як рэзультат „пасляпэўна выбранага“
- рэгіструе падпіску на хранілнік для гэтага компонента
- зноў выкананае селектар пасля кожнай дыстрабуіраванай дзеяння
- пораўнюе пакананы рэзультат з новым
- плануе паўтарны атрыбут толькі калі пораўнанне паказвае, што яны разныя
Шосты крок — гэтае месца, з якога выходзіць практычна весь неспадзяваныя працэсы.
Стандартны спосаб пораўнення — строгая рэферэнсная аднакавасць
За замовчаннем хук
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
Ваша задача — напісаць селектары, якія:
- вяртаюць толькі тые данні, якія викорыстоўвае компонент
- вяртаюць стабільныя рэферэнсы, калі нічы не зменілася
- не ствараюць новых об’ектоў чы рамакоў без патрэбы
- мемаізуюць вырахаваныя данні, якія складна вырахаваць
Першая правіла: ніколы не выбіраць корневы стан
useSelector(state => state)
Другая правіла: утримвацца ад стварэння об’ектаў-обгортак у селектарах
Такі селектар стварае новы об’ект пасля кожнага запуску:
useSelector(state => ({
user: state.user,
count: state.count
}))
Вяртайце такі об’ект толькі тады, калі вы яшчэ разумна яго паруеце з
shallowEqual
чы праходзіце яго чераз мемаізаваны селектар.
Трэцяя правіла: мемаізаваць дорогія вырахункі
Сортаванне, фільтрацыя, групаванне і з’еднанне належаць да
createSelector(...)
Чатвертыя правіла: выкорыстоўваць React.memo для элементаў
Перш чым мемаваць компонент, перагляньце короткі список пунктав:
Is the component expensive?
↓
Does it receive stable props?
↓
Does it re-render unnecessarily?
↓
Then consider React.memo()
Якщо ваша база коду викорыстоўвае React Compiler, большая частка такой ручной мемавацыі можа вже быць адбываная.
Пятыя правіла: выбіраць толькі тое, што дозволяе компонент
Занадта шырока выборка:
state => state
Краща:
state => state.auth.user
І ще краща, калі компонент паказвае толькі назву:
state => state.auth.user.name
Рэдкія крайнія прыклады: застарелыя пропы і „зомбі“-дзеці
Большасць аплікацыяў ніколи не сталкнуецца з гэтымі двума проблемамі, але гэта пояснюе, чаму вжыванне захоўнічых селектараў є хорашым звыкам.
Застарелыя пропы
Для рашэння проблемы застарелых пропаў патрэбны селектары, якія залежаць ад пропа, і апдэйт хранілніка, які зменяе як стан, так і, опосередкаўна, той проп:
Selector depends on component props
↓
Redux action updates state
↓
Parent would receive new props
↓
Child selector runs first
↓
Selector sees old props
Падчырый элемент можа выконваць свою функцыю раней, чым абяцельны элемент пераранжавае інфармацыю та передае новыя параметры. Некалькі момантамі селектар спалюеяе новы стан з старымі параметрами. Типовы прыклад такой:
const todo = useSelector(
state => state.todos[props.id]
)
Якщо элемент з такім id ўжо быў адключаны, або абяцельны элемент зараз перадае іншы id, селектар на час чытае данні, якія больш не падходзяць.
Стварэнне селектараў, якія перажываюць відсутнасць даных
Нестабільная версія прыпускае, што элемент завжды існуе:
state.todos[props.id].name
Захоўнайшая версія спачатку шукае элемент,
const todo =
state.todos[props.id]
а толькі пасля чытае з яго інфармацыю:
return todo
? todo.name
: undefined
Рашэнне ўжо ў форме простага пераканання:
Does data exist?
↓
Yes → use it
No → handle safely
Неабавязковая ланцюгова аперацыя (state.todos[id]?.name) выражае тую ж ідею ў адной операзыі.
„Зомбі“-падчырэйкі
Сцэнарый зомбі-дзеця включае абяцю, яка выканаляе спіс, і дзеця, якое падпішаеся на адзін элемент з гэтаго спісу:
Parent
│
└── Child
Працэўніца выглядае так:
- Дзеця падпішаеся на хранальнік.
- Якась дзеянне адчыняе данні, якія паказвае дзеця.
- Абяць перастане выканаляваць гэта дзеця пад час наступнага выканання.
- До таго часу дзеця продовжае працаваць за своім падпісам.
- Селектар дзеця прабуе знайсці данні, якія вже не існуюць.
Незахаваны селектар выклікае адзію на гэтым последнім кроцы. React-Redux мае механізмы для адрабатавання памылак селектара, якія выйшлі з-за адчынення даных у хранальніку, але захаваныя селектары застаюцца найнадзейным рашэнням.
Чаму хукі ў большай меры вразлівыя, чым connect
connect() стварае вяроўжанае дрэва падпіскаў: кожны з’яеднаны компонент апоштова адкладзець змены пасля таго, як апоштова адкладзяць змены ўсі яго з’яеднаныя праўёначнікі, што забезпечвае порядак «зверху вярху». Хукі прыўязуюцца безпосередня да хранілні без такой іерархіі, таму гарантіі парадку є слабейшыя, і такі краевы прыклады стаюць тэоретычна можлівымі.
Спрыяйце гэтаму як фонавым знанням, а не як прычыне для ухіленьства ад хуків. У документацыі зазначаецца, што такія проблемы є рэдкімі ў рэальных застосоўаннях.
Развітыя функцыі Provider
Адзінакавы контэкст для ізольаваных хранілняў
По значэнню стандарту,
<Provider store={store}>
хранілня публікуецца чераз вбудованный контэкст React-Redux. Бібліятэка компонентаў, якая викорыстоўвае Redux усередзіні, можа стварыць суперасунекі з хранілнёю гаспадарскага застосоўвання. Ёй ухіліцца, Provider прыме яшчо адзін ваш састаноўны контэкст:
<Provider
context={MyContext}
store={myStore}
>
Хукі, прыўязаныя да гэтага контексту, выйшлі з функцыяў-фабрык:
createStoreHook()
createDispatchHook()
createSelectorHook()
Гэта галоўная прычына проблем у бібліятэках, якія можна викорыстоўваць па-разным чынам, калі інакш кантэйнеры моглі бы стварыць суперскарыянне.
batch() і аўтаматычнае групаванне ў React 18
У старэйшым коде часта выкорыстоўваецца 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 запускаецца так часта?
Ён:
- запускаецца пад час адраджавання
- слухае за змянамі в магазіне
Такім чынам, інлайн-селектар, які ў кожны раз пад час адрасавання є новай функцыяй, выконваецца ў кожны раз; стабільная рэферэнцыя селектара дазволяе React-Redux праскочыць гэты вызов.
Што варуць вивучаць, па прыорітэтах
Рывень першы: неабходна абсалютная база
Provider
useSelector
useDispatch
Redux Store flow
Selectors
=== equality
Re-render behavior
Redux Toolkit + React-Redux
TypeScript
RootState
AppDispatch
.withTypes()
Рывень другі: моцныя практычныя навыкі
shallowEqual
Memoized selectors
createSelector
useStore
connect
mapStateToProps
mapDispatchToProps
ConnectedProps
React.memo
Рывень трэці: прыглушаныя тэмы
Stale props
Zombie children
Selector + props
Selector memoization
Custom context
Development mode checks
SSR serverState
batch()
Што не варуць запамятаваць
Вам не трэба вивучаць дасведчэнні па лініях. Можна праскочыць:
- спосаб рэалізацыі механізма падпіску ўнутрання
- рэдка выкорыстоўваныя опцыі
connect() - давно застарэлыя патэрны
- деталі рэалізацыі выхіднага коду
Важлівае — гэтыя правіла і прычыны, якія за ўсём стояць. Знанне этага факту
useSelector uses ===
менш корыстнае, чым возможнасць даўаць адпаведныя адказы
Why?
а саме:
Because returning a new object
creates a new reference.
гэта прыводзіць да певнага ланцаўка дзеянняй
New reference
↓
=== false
↓
re-render
Якщо вы разумеете гэты ланцоўка, вы можете самі вывести большасць іншых правілаў.
Дзесяць правілаў, якія трэба прыменяць
1. Чытаць данні за дапамогою useSelector
useSelector()
так компаненты чытаюць стан.
2. Запісваць данні за допамогою useDispatch
useDispatch()
так компаненты адправляюць дзеяння.
3. Абгорнуць всю аплікацыю ў Provider
<Provider store={store}>
4. Памяроць правілаў порэвання
useSelector → ===
connect → shallow comparison
5. Ніколі не выбіраць корневы стан
useSelector(state => state)
6. Будзьце астатрактывы з селекторамі, якія вяртаюць об’екты
useSelector(state => ({
...
}))
7. Зберагаць у памяці дорогі вырахаваны даны
Memoized selector
8. Следаваць правілам для селектараў
Pure
Predictable
Granular
9. Спачатку задаць типы, а потым хукі
Спачатку выведзеныя типы:
RootState
AppDispatch
AppStore
потым хукі прыкладнення, створаныя на ўснове іх:
useAppSelector
useAppDispatch
useAppStore
10. Іспользаваць сучасны падход
Redux Toolkit
+
React-Redux Hooks
Весь API на адной сторонцы
Компактныя справакі, якіе охопляюць основныя хукі, прыемы паўтарэння выданняя, типы TypeScript, стары API і працоўныя тэмы вышэйшага рангу:
┌───────────────────────────────────────────────┐
│ REACT-REDUX │
├───────────────────────────────────────────────┤
│ │
│ <Provider store={store}> │
│ ↓ │
│ Makes Redux available to React │
│ │
│ useSelector() │
│ ↓ │
│ READ state │
│ ↓ │
│ Default comparison: === │
│ │
│ useDispatch() │
│ ↓ │
│ DISPATCH actions │
│ │
│ useStore() │
│ ↓ │
│ Direct store access │
│ ↓ │
│ Use rarely │
│ │
├───────────────────────────────────────────────┤
│ PERFORMANCE │
├───────────────────────────────────────────────┤
│ │
│ Avoid state => state │
│ Avoid unnecessary object creation │
│ Use granular selectors │
│ Use shallowEqual when appropriate │
│ Use memoized selectors for derived data │
│ Use React.memo when justified │
│ │
├───────────────────────────────────────────────┤
│ TYPESCRIPT │
├───────────────────────────────────────────────┤
│ │
│ RootState = ReturnType<typeof store.getState> │
│ AppDispatch = typeof store.dispatch │
│ AppStore = typeof store │
│ │
│ useAppSelector │
│ useAppDispatch │
│ useAppStore │
│ │
├───────────────────────────────────────────────┤
│ LEGACY / EXISTING APPLICATIONS │
├───────────────────────────────────────────────┤
│ │
│ connect() │
│ mapStateToProps │
│ mapDispatchToProps │
│ ConnectedProps │
│ │
├───────────────────────────────────────────────┤
│ ADVANCED │
├───────────────────────────────────────────────┤
│ │
│ Stale Props │
│ Zombie Children │
│ Custom Context │
│ SSR serverState │
│ Development checks │
│ batch() │
│ │
└───────────────────────────────────────────────┘
І цыкл, знова паказаны з шляхамі чытання і запісу па бокі:
REACT
│
│
<Provider />
│
▼
┌─────────────┐
│ Redux Store │
└──────┬──────┘
│
┌───────────┴───────────┐
│ │
▼ ▲
useSelector() useDispatch()
│ │
│ │
READ ACTION
│ │
│ │
│ ┌────┴─────┐
│ │ Reducer │
│ └────┬─────┘
│ │
│ ▼
│ New State
│ │
└───────────────────────┘
│
▼
Equality Check
│
┌──────┴──────┐
│ │
Same Changed
│ │
▼ ▼
No Redux Re-render
render
Для новага проекту на TypeScript рэкамендаваная наладка выглядае так:
Redux Toolkit
+
React-Redux
│
┌──────────┴──────────┐
│ │
Store Provider
│ │
│ Application tree
│ │
└──────────┬──────────┘
│
┌────────┴────────┐
│ │
useAppSelector() useAppDispatch()
│ │
READ WRITE
│ │
└────────┬────────┘
│
Redux
│
New State
│
Selector
│
Re-render
Заключанне
React-Redux стае набаго простым, калі перестануце спрыягаць яго экспорты як незв’язаныя інструменты. этый список назв
Provider
useSelector
useDispatch
connect
shallowEqual
createSelector
useStore
Апісвае адзін пайплайн з адной задачай на кожны ўраг. Праўодзіцель адкрывае хранэнне:
Provider
↓
makes Store available
Хук селектара чытае:
useSelector
↓
reads selected state
Хук дыспатча адправляе:
useDispatch
↓
sends actions
Редюсеры ствараюць наступны стан:
Reducer
↓
creates new state
Селектары формуюць яго для 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 мелі модэль для прыўязкі.
Супаўзвяжаная літэратура
- React Query і Redux: Наватарэнне пад станам сервера ў вялікіх дапрацоўках — Дазвольце дазнацца, чаму дапрацоўка чата для прымення выкорыставала TanStack Query заместо Redux для керування дадзеннямі сервера, і дзе Redux яшчэ мае месца ў сучасной архітектуры React.
- Структураванне шара дадзення TanStack Query, ад queryOptions да Rollbacks — Построюйце шар дадзення TanStack Query па кроках: спяльныя queryOptions, фабрыкі ключоў, селектары, пагінацыя, запрашэнне дадзення заздалегідь, централізавана недзейнасць і безпечныя оптымістычныя апдэйты.