У світі Redux селектори часто недооцінюють. Багато розробників сприймають їх просто як функції для отримання шматочка даних зі стейту. Але насправді, селектори — це потужний шар абстракції, який відповідає за ефективність, інкамисуляцію та обчислення похідних даних.
Коли ваш додаток розростається (особливо це стосується Enterprise-проєктів з тисячами сутностей), бездумна вибірка даних призводить до катастрофічних втрат продуктивності. Компоненти ре-рендеряться без потреби, обчислення повторюються тисячі разів на секунду, а інтерфейс починає "гальмувати" при кожному натисканні клавіші.
Цей гайд повністю розкриє тему селекторів: від базової філософії до складних патернів мемоізації з Reselect, типізації в TypeScript та архітектурних рішень.
Формально, селектор — це будь-яка чиста функція, яка приймає весь стейт Redux (або його частину) і повертає значення.
// Найпростіший селектор
const selectUser = (state) => state.user
// Селектор з обчисленням
const selectUserAge = (state) => {
const birthDate = new Date(state.user.birthDate)
const today = new Date()
return today.getFullYear() - birthDate.getFullYear()
}
Але концептуально, селектори — це Query Layer (Шар Запитів) для вашого стейту.
Чому це важливо?
state.user на state.auth.user, вам потрібно буде оновити лише селектор, а не 50 компонентів.Одне з найважливіших правил Redux: зберігайте в стейті мінімум даних. Все інше обчислюйте.
Приклад: У нас є список товарів і фільтр.
items і окремо filteredItems.items і filter. filteredItems обчислювати селектором.Чому? Тому що якщо ви додасте новий товар, вам доведеться оновлювати і items, і filteredItems. Це порушення Single Source of Truth.
Це "серце" проблеми продуктивності в React + Redux. Якщо ви не зрозумієте цей розділ, ви не зрозумієте Reselect.
useSelector?Хук useSelector підписується на оновлення Redux Store. Щоразу, коли будь-яка екшен викликається і стейт оновлюється, useSelector:
Порівняння відбувається через сувору рівність (===).
Розглянемо найпоширенішу помилку:
// ❌ ПОГАНО: Завжди повертає нове посилання
const selectActiveTodos = (state) => {
// .filter() ЗАВЖДИ створює новий масив, навіть якщо вміст не змінився!
return state.todos.filter((todo) => !todo.completed)
}
Що відбувається під капотом:
dispatch({ type: 'UI/THEME_CHANGED' }).todos ті самі).useSelector запускає selectActiveTodos.state.todos.filter(...) запускається і створює новий масив Array(5).Array(5).newArray === oldArray -> false.Ми змінили тему, а всі компоненти списку завдань перемалювалися. Це і називається "wasted renders".
| Операція | Чому це погано в селекторі? |
|---|---|
.map(...) | Створює новий масив. |
.filter(...) | Створює новий масив. |
[...array] | Створює новий масив. |
{ ...object } | Створює новий об'єкт. |
obj || {} | Якщо obj falsy, створює новий об'єкт {}. |
Date.parse(...) | Числа примітивні, це ОК. Але new Date() - це новий об'єкт. |
const state = {
items: [1, 2, 3],
}
// Виклик 1
const result1 = state.items.map((x) => x * 2)
// result1 = [2, 4, 6]
// Виклик 2 (стейт не змінився!)
const result2 = state.items.map((x) => x * 2)
// result2 = [2, 4, 6]
// Перевірка
console.log(result1 === result2) // FALSE ❌
Для React це означає: "Дані змінилися, треба малювати заново".
Мемоізація — це техніка оптимізації, яка кешує результат виконання функції на основі її аргументів. Якщо аргументи не змінилися, функція повертає закешований результат, не виконуючи обчислень.
Бібліотека Reselect (яка тепер вбудована в Redux Toolkit) надає функцію createSelector для створення мемоізованих селекторів.
createSelectorimport { createSelector } from '@reduxjs/toolkit'
const selectMemoizedData = createSelector(
// [A] Input Selectors (Залежності)
[inputSelector1, inputSelector2],
// [B] Result Function (Трансформатор)
(result1, result2) => {
// [C] Важкі обчислення
return doExpensiveCalculation(result1, result2)
},
)
Алгоритм роботи:
selectMemoizedData(state), Reselect спочатку запускає всі функції з масиву [A].===) попереднім, Reselect не запускає Result Function [B] і одразу повертає збережений результат.[B], зберігає новий результат і повертає його.const selectTodos = (state) => state.todos
// ✅ ДОБРЕ: Мемоізований селектор
const selectActiveTodos = createSelector([selectTodos], (todos) => {
console.log('Фільтрація...') // Виконається тільки якщо todos зміняться
return todos.filter((todo) => !todo.completed)
})
Тепер, якщо ми змінимо тему (UI/THEME_CHANGED):
selectTodos поверне той самий масив todos (бо він не змінився).createSelector побачить, що аргумент не змінився.useSelector побачить, що посилання те саме -> Re-render скасовано. 🎉Справжня сила селекторів розкривається в їх компонуванні. Ви можете будувати селектори на базі інших селекторів.
Уявіть собі піраміду:
// --- Рівень 0/1: Input Selectors ---
const selectAllTodos = (state) => state.todos.items
const selectFilter = (state) => state.todos.filter
const selectSearchQuery = (state) => state.todos.search
// --- Рівень 2: Фільтрація ---
const selectFilteredTodos = createSelector(
[selectAllTodos, selectFilter, selectSearchQuery],
(todos, filter, query) => {
// Важка логіка фільтрації
let result = todos
if (filter === 'completed') {
result = result.filter((t) => t.completed)
}
if (query) {
result = result.filter((t) => t.title.includes(query))
}
return result
},
)
// --- Рівень 3: Статистика ---
const selectTodoStats = createSelector(
[selectFilteredTodos], // Залежить від вже фільтрованих!
(todos) => {
return {
count: todos.length,
urgent: todos.filter((t) => t.priority === 'high').length,
}
},
)
// --- Рівень 4: View Model ---
const selectTodoListViewModel = createSelector([selectFilteredTodos, selectTodoStats], (todos, stats) => ({
items: todos,
subtitle: `Shown ${stats.count} items (${stats.urgent} urgent)`,
}))
Як це працює при оновленні?
Припустимо, користувач змінив searchQuery.
selectSearchQuery -> Нове значення.selectFilteredTodos -> Input змінився -> Перераховує фільтрацію -> Новий масив todos.selectTodoStats -> Input змінився -> Перераховує статистику.selectTodoListViewModel -> Inputs змінилися -> Оновлює View Model.А тепер припустимо, користувач змінив своє ім'я в профілі (user.name).
selectSearchQuery -> Те саме. selectAllTodos -> Те саме. selectFilter -> Те саме.selectFilteredTodos -> Inputs не змінилися -> Cache Hit. Повертає старий масив.selectTodoStats -> Input (результат попереднього) не змінився -> Cache Hit.selectTodoListViewModel -> Inputs не змінилися -> Cache Hit.Reselect надає потужні можливості для налаштування того, як саме працює мемоізація.
lruMemoize vs weakMapMemoizeЗа замовчуванням createSelector використовує стратегію LRU (Least Recently Used) з розміром кешу 1.
lruMemoizeЦе стандартна функція мемоізації. Вона зберігає останні N викликів.
import { createSelector, lruMemoize } from '@reduxjs/toolkit'
const selectItems = createSelector([input], resultFunc, {
memoize: lruMemoize,
memoizeOptions: {
equalityCheck: (a, b) => a === b, // Стандартна перевірка
maxSize: 1, // Розмір кешу
resultEqualityCheck: (a, b) => a === b, // Перевірка результату
},
})
Коли змінювати maxSize?
Якщо один і той же селектор використовується в різних місцях з різними аргументами (але не одночасно), збільшення maxSize може допомогти уникнути перерахунків при перемиканні контексту.
weakMapMemoizeНова опція в Reselect 5.0. Вона використовує WeakMap для зберігання результатів.
Особливості:
Це ідеально підходить, коли аргументом селектора є сам стейт або великий об'єкт пропсів.
import { createSelector, weakMapMemoize } from '@reduxjs/toolkit'
const selectData = createSelector([(state) => state.someObject], (obj) => complexCalculation(obj), {
memoize: weakMapMemoize, // Використовуємо WeakMap
argsMemoize: weakMapMemoize, // Для аргументів теж
})
equalityCheck)Іноді === недостатньо. Наприклад, якщо input selector повертає новий масив, але його вміст ідентичний.
import { createSelector } from '@reduxjs/toolkit'
import { isEqual } from 'lodash'
const selectDeep = createSelector([(state) => state.deeplyNestedObject], (obj) => calculate(obj), {
memoizeOptions: {
// ⚠️ Use with caution! Deep comparison is slow!
equalityCheck: isEqual,
},
})
Застереження: Глибоке порівняння (deep equality) може бути повільним саме по собі. Іноді швидше просто перерахувати селектор, ніж порівнювати два величезних дерева об'єктів.
Іноді селектору потрібні дані не тільки зі стейту, але й з параметрів компонента (наприклад, id продукту).
const selectProductById = (state, productId) => state.products.entities[productId]
Це працює, але як це мемоізувати? createSelector дозволяє передавати додаткові аргументи. Аргументи з useSelector передаються в Input Selectors як другий, третій і т.д. аргументи.
// Input Selectors
const selectItems = (state) => state.items
const selectItemId = (state, itemId) => itemId // Просто повертаємо ID
const selectItemById = createSelector(
[selectItems, selectItemId], // Передаємо обидва
(items, itemId) => {
console.log(`Searching for item ${itemId}`)
return items.find((item) => item.id === itemId)
},
)
Використання в компоненті:
function ItemComponent({ id }) {
// Передаємо state ТА id
const item = useSelector((state) => selectItemById(state, id))
return <div>{item.name}</div>
}
За замовчуванням createSelector має розмір кешу = 1. Це означає, що він пам'ятає лише останній набір аргументів.
Якщо ви використовуєте цей селектор у списку компонентів:
function ItemList() {
return (
<>
<ItemComponent id={1} />
<ItemComponent id={2} />
<ItemComponent id={1} />
</>
)
}
Що відбувається:
ItemComponent(1) викликає селектор з id=1. Кешується результат для 1.ItemComponent(2) викликає селектор з id=2. Аргументи змінилися! Кеш перезаписується для 2.ItemComponent(1) (ре-рендер) викликає селектор з id=1. Аргументи змінилися (порівняно з 2)! Кеш перезаписується.Результат: Мемоізація повністю зламана. Селектор постійно перераховується. Це називається Selector Thrashing.
Щоб вирішити проблему shared cache, кожному екземпляру компонента потрібен власний екземпляр селектора.
Ми використовуємо патерн Factory Function — функція, яка створює селектор.
// 1. Створюємо функцію-фабрику (make...)
const makeSelectItemById = () => {
// 2. Всередині створюємо НОВИЙ createSelector
return createSelector([(state) => state.items, (state, id) => id], (items, id) => {
console.log('Complex finding logic...')
return items.find((i) => i.id === id)
})
}
Використання в компоненті з useMemo:
function ItemComponent({ id }) {
// 1. Створюємо унікальний селектор для ЦЬОГО компонента
// useMemo гарантує, що селектор створиться лише один раз на життя компонента
const selectItemById = useMemo(() => makeSelectItemById(), []) // [] - без залежностей
// 2. Викликаємо цей унікальний селектор
const item = useSelector((state) => selectItemById(state, id))
return <div>{item?.name}</div>
}
Тепер:
(state, 1) стабільні.(state, 2) стабільні.Мемоізація працює ідеально!
Що робити, якщо пропси змінюються? (наприклад, filterType).
const makeSelectVisibleTodos = () => {
return createSelector([(state) => state.todos, (state, props) => props.filterType], (todos, filterType) => {
switch (filterType) {
case 'SHOW_COMPLETED':
return todos.filter((t) => t.completed)
case 'SHOW_ACTIVE':
return todos.filter((t) => !t.completed)
default:
return todos
}
})
}
function VisibleTodoList({ filterType }) {
const selectVisibleTodos = useMemo(makeSelectVisibleTodos, [])
const todoList = useSelector((state) => selectVisibleTodos(state, { filterType }))
// ...
}
Якщо filterType зміниться, селектор просто перерахує значення і оновить свій внутрішній кеш.
Часто компоненту потрібно отримати декілька шматків даних. Початківці роблять так:
// ❌ 3 виклики хука
const user = useSelector(selectUser)
const theme = useSelector(selectTheme)
const notifications = useSelector(selectNotifications)
Або так:
// ❌ Повертає новий об'єкт щоразу -> re-render
const { user, theme } = useSelector((state) => ({
user: state.auth.user,
theme: state.ui.theme,
}))
Рішення 1: shallowEqual
Можна використати функцію порівняння.
import { shallowEqual, useSelector } from 'react-redux'
const { user, theme } = useSelector(
(state) => ({
user: state.auth.user,
theme: state.ui.theme,
}),
shallowEqual,
) // ✅ Порівнює вміст об'єкта, а не посилання
Рішення 2: Структурований селектор з ReselectcreateSelector може повертати об'єкти, і оскільки він мемоізований, посилання на цей об'єкт буде стабільним.
const selectUserData = createSelector([selectUser, selectTheme], (user, theme) => ({
user,
theme,
isDark: theme === 'dark',
}))
// В компоненті
const { user, isDark } = useSelector(selectUserData)
Це краще, тому що селектор може інкапсулювати логіку комбінування даних.
Робота з деревовидними структурами (наприклад, коментарі з вкладеністю) вимагає особливого підходу.
Припустимо, у нас є плоский стейт коментарів (нормалізований):
const state = {
comments: {
1: { id: 1, text: 'Hello', parentId: null },
2: { id: 2, text: 'World', parentId: 1 },
3: { id: 3, text: '!!!!!', parentId: 2 },
},
}
Ми хочемо побудувати дерево.
const selectComments = (state) => state.comments
const selectCommentTree = createSelector([selectComments], (comments) => {
// 1. Створюємо мапу
const map = {}
const roots = []
Object.values(comments).forEach((c) => {
map[c.id] = { ...c, children: [] } // Створюємо копію з children
})
// 2. Будуємо дерево
Object.values(map).forEach((c) => {
if (c.parentId && map[c.parentId]) {
map[c.parentId].children.push(c)
} else {
roots.push(c)
}
})
return roots // Повертає дерево
})
Цей селектор поверне нове дерево щоразу, коли зміниться хоча б один коментар. Це нормально для більшості випадків. Але якщо дерево величезне, можна оптимізувати це, мемоізуючи окремі гілки, хоча це значно ускладнює код.
У 2023 році вийшов Reselect 5.0 (частина RTK 2.0). Він приніс важливі інструменти для дебаггінгу.
У режимі розробки (process.env.NODE_ENV !== 'production') Reselect тепер автоматично перевіряє ваші селектори на типові помилки.
// ⚠️ Console Warn in Dev: The result function returned a new reference...
const selectBad = createSelector(
[(state) => state.items.map((i) => i.id)], // map повертає новий масив!
(ids) => ids,
)
createSelector(..., { devModeChecks: { inputStabilityCheck: 'never' } }).createSelector([selectData], (data) => data) // Навіщо тут createSelector?
Reselect написаний на TS, тому типізація чудова, але іноді складна.
import { RootState } from './store'
// Явно вказуємо тип state
const selectCount = (state: RootState) => state.counter.value
// createSelector виводить типи автоматично
const selectDoubleCount = createSelector(
[selectCount],
(count) => count * 2, // TS знає, що count це number
)
// Типізуємо пропси
const selectItemById = createSelector(
[
(state: RootState) => state.items,
(state: RootState, itemId: string) => itemId, // Вказуємо тип другого аргументу
],
(items, itemId) => items.find((i) => i.id === itemId),
)
Іноді вам потрібно експортувати тип самого селектора.
import { OutputSelector } from '@reduxjs/toolkit';
export const selectUser: OutputSelector<
[typeof selectAuth], // Inputs
User, // Result
(res: AuthState) => User // Combiner
> = createSelector(...)
(Зазвичай typeof selectUser достатньо).
export const makeSelectUserById = () => {
return createSelector([selectUsers, (state: RootState, id: number) => id], (users, id) => users[id])
}
// Тип результату фабрики
type SelectUserById = ReturnType<typeof makeSelectUserById>
Де зберігати селектори? Є два підходи.
Зберігайте селектори поруч з редюсером у файлі слайсу.
// features/users/usersSlice.js
const usersSlice = createSlice({ ... });
// Приватні input selectors
const selectUsersState = state => state.users;
// Публічні експортовані селектори
export const selectAllUsers = createSelector(
[selectUsersState],
state => state.ids.map(id => state.entities[id])
);
export default usersSlice.reducer;
Для великих додатків можна винести селектори в usersSelectors.js. Це допомагає уникнути циклічних залежностей, якщо селектори одного слайсу залежать від іншого.
selectors полеВ новій версії RTK ви можете визначати селектори прямо в createSlice:
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: { ... },
// Нове поле!
selectors: {
selectDouble: (state) => state.value * 2,
selectQuadruple: (state) => state.value * 4,
},
});
// Автоматичне генерування імен (але можна деструктурувати)
export const { selectDouble, selectQuadruple } = counterSlice.selectors;
Важливо: state тут — це локальний стейт слайсу, а не глобальний RootState.
// ❌ ЖАХ
function UserProfile() {
// Цей селектор створюється заново при кожному рендері
// Його кеш завжди порожній!
const selectUser = createSelector(...);
const user = useSelector(selectUser);
}
Рішення: Винесіть селектор за межі компонента або використовуйте useMemo (Factory Pattern).
createSelector для простих операцій// 😐 ПЕРЕБІР
const selectCount = createSelector([(state) => state.counter], (counter) => counter.value)
Це оверхед. Прості селектори state => state.value працюють швидше, бо виклик функції дешевший за логіку мемоізації. Використовуйте createSelector тільки якщо є:
undefined для дефолтних значень// ❌ Може повернути новий об'єкт {} при кожному виклику, якщо юзера немає
const selectUser = createSelector([selectState], (state) => state.user || {})
Рішення: Винесіть дефолтне значення в константу.
const EMPTY_USER = {}
const selectUser = createSelector([selectState], (state) => state.user || EMPTY_USER)
Що робити, якщо селектор не мемоізується, і ви не розумієте чому?
Існує бібліотека re-reselect (або просто додайте логування), яка дозволяє перевірити кеш.
Але простіше додати лог в сам селектор:
const selectComplex = createSelector([input1, input2], (res1, res2) => {
console.log('Recalculating!', { res1, res2 }) // Якщо ви бачите це занадто часто — проблема в inputs
return res1 + res2
})
useSelector — ваш селектор повернув нове посилання.Ви можете перевірити кількість ре-обчислень:
selectComplex.recomputations() // Кількість разів, коли resultFunc виконувалась
selectComplex.resetRecomputations() // Скинути лічильник
Це корисно в тестах або консолі.
Оскільки селектори — це чисті функції, тестувати їх одне задоволення. Вам не потрібен Redux Store, компоненти чи моки. Просто вхідні дані і очікуваний результат.
// selectors.test.js
import { selectVisibleOrders } from './ordersSlice'
describe('selectVisibleOrders', () => {
it('should filter by status', () => {
const orders = [
{ id: 1, status: 'pending' },
{ id: 2, status: 'completed' },
]
const filters = { status: 'pending', sort: 'date' }
// Викликаємо result function напряму для ізольованого тестування!
// createSelector.resultFunc доступна для тестів
const result = selectVisibleOrders.resultFunc(orders, filters)
expect(result).toHaveLength(1)
expect(result[0].id).toBe(1)
})
it('should act as a memoized selector', () => {
const state = {
/* ... */
}
const result1 = selectVisibleOrders(state)
const result2 = selectVisibleOrders(state)
expect(result1).toBe(result2) // Reference equality check
})
})
Або тестуйте як звичайну функцію, передаючи повний стейт:
const state = {
orders: { list: [...], filters: {...} }
};
const result = selectVisibleOrders(state);
async в селекторах?В: НІ! Селектори повинні бути чистими синхронними функціями. Якщо вам потрібно робити асинхронні запити, використовуйте Thunks або RTK Query. Селектор працює тільки з тим, що вже є в стейті.
В: НІ! Селектор — це звичайна функція JS. Хуки (useSelector, useState) працюють тільки всередині компонентів або інших хуків.
В: Передавайте аргументи як об'єкт.
const selectItem = (state, { id, type, role }) => ...
Аргументи для createSelector будуть: (state, props) => props.xyz.
В: Так! createSelector — це незалежна утиліта.
// Zustand example
import { createStore } from 'zustand';
import { createSelector } from 'reselect'; // або @reduxjs/toolkit
const useStore = createStore((set) => ({ ... }));
const selectDoubled = createSelector(
[state => state.count],
count => count * 2
);
// В компоненті
const doubled = useStore(selectDoubled);
В: Ні. Мемоізація має свою ціну (пам'ять, перевірки). Не мемоізуйте прості доступи (state => state.x) або дуже дешеві операції (a + b). Мемоізуйте:
Хочете перевірити, наскільки селектори швидші? Запустіть цей скрипт у Node.js.
/* benchmark.js */
const { createSelector } = require('@reduxjs/toolkit')
// 1. Створимо великий масив
const ITEMS_COUNT = 100000
const state = {
items: Array.from({ length: ITEMS_COUNT }, (_, i) => ({
id: i,
completed: Math.random() > 0.5,
value: Math.random() * 100,
})),
}
// 2. Звичайний селектор
const selectBad = (state) => state.items.filter((i) => i.completed)
// 3. Мемоізований селектор
const selectMemoized = createSelector([(state) => state.items], (items) => items.filter((i) => i.completed))
// 4. Тест продуктивності
console.time('Bad Selector (First Run)')
selectBad(state)
console.timeEnd('Bad Selector (First Run)')
console.time('Bad Selector (Second Run)')
selectBad(state) // Перераховує знову!
console.timeEnd('Bad Selector (Second Run)')
console.time('Memoized (First Run)')
selectMemoized(state)
console.timeEnd('Memoized (First Run)')
console.time('Memoized (Second Run)')
selectMemoized(state) // Миттєво!
console.timeEnd('Memoized (Second Run)')
/*
Очікуваний результат:
Bad Selector (First Run): 5.2ms
Bad Selector (Second Run): 5.1ms
Memoized (First Run): 5.3ms
Memoized (Second Run): 0.005ms 🚀 (1000x швидше!)
*/
Давайте розглянемо реальний приклад з фінансової сфери (крипто-біржа), де неправильні селектори "вбивають" CPU.
У нас є WebSocket, який присилає 100 оновлень цін на секунду. Стейт виглядає так:
/* state.market */
{
tickers: {
'BTC-USDT': { price: 45000.50, volume: 1000 },
'ETH-USDT': { price: 3200.10, volume: 5000 },
// ... ще 500 пар
},
favorites: ['BTC-USDT', 'DOGE-USDT'] // Список улюблених
}
// Компонент списку улюблених
function FavoritesList() {
// ❌ Цей селектор створює новий масив 100 разів на секунду!
const favorites = useSelector((state) => {
return state.market.favorites.map((symbol) => state.market.tickers[symbol])
})
return (
<div>
{favorites.map((ticker) => (
<TickerRow key={ticker.symbol} data={ticker} />
))}
</div>
)
}
Що відбувається?
XRP-USDT (якої навіть немає в favorites).state.market.tickers).useSelector запускається..map(...) і створює новий масив об'єктів (навіть якщо ціни BTC і DOGE не змінилися!).FavoritesList ре-рендериться 100 разів на секунду.Ми повинні ізолювати дані. Якщо змінився XRP, селектор favorites не повинен перераховуватись.
// 1. Input selectors
const selectTickers = (state) => state.market.tickers
const selectFavoritesIDs = (state) => state.market.favorites
// 2. Memoized selector
const selectFavoriteTickers = createSelector(
[selectTickers, selectFavoritesIDs],
(tickers, favoriteIds) => {
return favoriteIds.map((id) => tickers[id])
},
{
// 🚨 Цього все ще НЕДОСТАТНЬО!
// Якщо tickers змінився (через XRP), то перший аргумент змінився.
// Reselect скине кеш і перерахує масив.
},
)
Ми все ще маємо проблему! state.market.tickers — це один великий об'єкт. Будь-яка зміна будь-якої ціни змінює посилання на цей об'єкт.
Замість того, щоб вибирати весь список, компонент списку повинен вибирати тільки ID, а дочірні компоненти — тільки свої дані.
// Father Component
function FavoritesList() {
// ✅ Цей масив змінюється рідко (тільки коли юзер додає/видаляє зірок)
const favoriteIds = useSelector((state) => state.market.favorites)
return (
<div>
{favoriteIds.map((id) => (
<MemoizedTickerRow key={id} symbol={id} />
))}
</div>
)
}
// Child Component
const makeSelectTickerData = () => {
return createSelector(
[(state) => state.market.tickers, (state, symbol) => symbol],
(tickers, symbol) => tickers[symbol],
)
}
const TickerRow = ({ symbol }) => {
// ✅ Factory pattern: кожен рядок має свій селектор
const selectTickerData = useMemo(makeSelectTickerData, [])
const data = useSelector((state) => selectTickerData(state, symbol))
return (
<div>
{symbol}: {data.price}
</div>
)
}
// Не забуваємо React.memo, щоб батьківський рендер не чіпляв дітей
const MemoizedTickerRow = React.memo(TickerRow)
Результат:
XRP.state.market.tickers оновлюється.FavoritesList дивиться на state.market.favorites. Він не змінився. Рендер списку не відбувається.TickerRow (BTC, DOGE) перевіряють свої селектори.selectTickerData для BTC дивиться: tickers змінилися? Так. symbol змінився? Ні.tickers['BTC-USDT']. Порівнює об'єкт ціни з попереднім.tickers правильно (через Immer), то tickers['BTC'] залишиться тим самим об'єктом, якщо ми не чіпали його поля.selectTickerData поверне старий результат.
useSelector не викличе ре-рендер TickerRow(BTC).Вся система залишається спокійною, навіть при шаленому потоці даних. Оновлюється (ре-рендериться) тільки TickerRow(XRP), якщо він є на екрані.
Селектори — це не просто спосіб взяти дані. Це фундамент продуктивності React-Redux додатку.
useSelector з розумом: Слідкуйте за референційною цілісністю.createSelector ваш найкращий друг.Опанувавши селектори, ви зможете будувати додатки, які залишаються швидкими навіть при тисячах елементів і складній бізнес-логіці.
Quiz: Перевірка знань
Наступна тема: RTK Query
04. Entity Adapter: Керування нормалізованим станом
RTK Query: Архітектура Серверного Кешу
RTK Query — це не просто "fetcher" даних. Це повноцінний Server State Manager, який інтегрований у Redux Toolkit. Він перевертає уявлення про те, як ми працюємо з даними на клієнті, зміщуючи фокус з "ручного управління loading/error/data" на "декларативне описування залежностей".