Що таке selectors в Redux?
Selectors у Redux є чистими функціями, які зчитують конкретні дані зі стану store і за потреби трансформують їх перед тим, як компонент їх отримує.
Теорія
TL;DR
- Selector: функція що приймає
stateі повертає його частину - Прості selectors обчислюються на кожному рендері; мемоїзовані (Reselect) пропускають роботу якщо вхідні дані не змінились
createSelectorварто використовувати для будь-яких похідних даних: відфільтрованих списків, обчислених сум, сортування- Аналогія: меню в ресторані, де ти замовляєш саме те що потрібно, а не весь склад кухні
Швидкий приклад
// Простий selector - запускається на кожному рендері
const getUsers = (state) => state.users;
// Мемоїзований selector - перераховується тільки якщо посилання на users змінилось
import { createSelector } from 'reselect';
const getActiveUsers = createSelector(
[getUsers],
(users) => users.filter(u => u.active)
);
// У компоненті
const activeUsers = useSelector(getActiveUsers);
// Повертає [{id:1, active:true}] - закешовано до зміни посилання на usersПісля першого виклику getActiveUsers зберігає результат. При наступному рендері, якщо посилання на state.users те саме, повертається закешований масив без повторного запуску .filter().
Простий vs мемоїзований
Простий selector виконується щоразу коли спрацьовує useSelector, тобто при кожному оновленні store. Для прямого доступу до полів state це нормально. Але варто додати .filter(), .map() або будь-яке обчислення, і ця робота виконується на кожному рендері, навіть якщо в тому зрізі нічого не змінилось.
Одного разу я відстежував компонент що ре-рендерився 200 разів на секунду в продакшн дашборді. Причиною виявився єдиний selector що повертав новий масив при кожному виклику, хоча вихідні дані не змінились.
createSelector з Reselect обгортає обчислення в кеш, де ключем слугує результат вхідних selectors. Перевірка рівності за посиланням. Якщо перевірка пройшла, повертається закешований результат. Це перетворює повторну O(n) роботу на майже миттєве звернення до кешу.
Коли використовувати
- Прямий доступ до state (одне поле, без трансформацій): простий selector, бібліотека не потрібна
- Похідні або обчислені значення (відфільтровані списки, суми, сортування): мемоїзований через Reselect
- Одні дані в кількох компонентах: shared мемоїзований selector тримає логіку в одному місці
- Великий state tree з частими рендерами: мемоїзований, щоб зупинити каскадні ре-рендери
Як Reselect працює зсередини
createSelector приймає масив вхідних selectors і функцію результату. При кожному виклику запускає вхідні selectors і порівнює їхні результати з попередніми через перевірку рівності за посиланням. Якщо всі результати збіглись, функція результату не виконується і повертається закешоване значення. Якщо хоч один вхідний selector повернув нове посилання, функція результату запускається знову.
Важливий нюанс: розмір кешу за замовчуванням дорівнює 1. Кожен selector зберігає тільки останній результат. Для selectors що обробляють кілька екземплярів, наприклад per-item selectors у списку, використовуй factory pattern: const makeUserSelector = (id) => createSelector([getUsers], users => users[id]).
Типові помилки
1. Мутація state всередині selector
// Неправильно - порушує чистоту і перевірку рівності за посиланням
const getProcessed = createSelector([getUsers], (users) => {
users[0].name = 'mutated';
return users;
});
// Правильно - повертай нове посилання
const getProcessed = createSelector([getUsers], (users) =>
users.map(u => ({ ...u, processed: true }))
);Мутація ламає перевірку рівності за посиланням, на якій тримається мемоїзація. Результат: застарілі дані або пропущені ре-рендери.
2. Пропущені вхідні selectors
// Неправильно - getPosts обходить кеш повністю
createSelector([getUsers], getPosts, (users, posts) => ...);
// Правильно - вказуй всі inputs у масиві
createSelector([getUsers, getPosts], (users, posts) => ...);Reselect відстежує тільки inputs перераховані в масиві. Решта передаються як аргументи але не спостерігаються.
3. Створення selector всередині компонента
// Неправильно - нова інстанція selector на кожному рендері, кеш скидається
const MyComponent = () => {
const getFiltered = createSelector(
[getUsers],
u => u.filter(x => x.active)
);
const users = useSelector(getFiltered);
};
// Правильно - визначай поза компонентом
const getFilteredUsers = createSelector(
[getUsers],
u => u.filter(x => x.active)
);4. Відсутність shallowEqual для об'єктних selectors
// Може спричиняти зайві ре-рендери якщо selector повертає новий об'єкт
useSelector(state => state.complexObject);
// Краще
import { shallowEqual } from 'react-redux';
useSelector(state => state.complexObject, shallowEqual);useSelector за замовчуванням використовує перевірку рівності за посиланням. Selector що повертає новий об'єкт при кожному виклику (навіть з однаковими значеннями) запустить ре-рендер.
Де зустрічається в реальних проектах
- E-commerce:
useSelector(getCartTotal)обчислює суму кошика без дублювання логіки по компонентах - Todo-додатки:
getVisibleTodosз фільтром видимості (офіційні приклади Redux використовують саме цей патерн) - Нормалізований store: вибірка entities за ID, об'єднання пов'язаних даних з кількох slices
- Zustand для невеликих додатків до 10 selectors; Redux разом з Reselect для нормалізованих store зі складним похідним станом
Питання на співбесіді
Q: Що станеться якщо повертати новий масив з useSelector без Reselect?
A: Компонент ре-рендериться при кожному оновленні store, навіть якщо дані ідентичні. Нове посилання на масив не проходить перевірку рівності яку useSelector застосовує за замовчуванням.
Q: Як працює композиція selectors?
A: Результат одного selector стає входом для наступного. createSelector([getVisible], getCount) мемоїзує незалежно на кожному рівні. Зміна в джерелі піднімається тільки через ті selectors що від нього залежать.
Q: Коли мемоїзований selector перераховується?
A: Коли будь-який вхідний selector повертає нове посилання. Shallow equality для масивів і об'єктів означає що мутація in-place не запустить перерахунок. Це поширений баг при оновленнях state без immer.
Q: (Senior) Чим відрізняються selectors в RTK Query від Reselect?
A: RTK Query автоматично генерує tag-based selectors прив'язані до записів кешу, включно зі станами loading та error. Reselect залишай для domain логіки поза API-запитами, наприклад фільтрація або сортування вже закешованих даних.
Приклади
Базовий: фільтрація активних користувачів
import { createSelector } from 'reselect';
import { useSelector } from 'react-redux';
// Вхідний selector
const getUsers = (state) => state.users;
// Похідний selector
const getActiveUsers = createSelector(
[getUsers],
(users) => users.filter(u => u.active)
);
function ActiveUserList() {
const activeUsers = useSelector(getActiveUsers);
return (
<ul>
{activeUsers.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
// Якщо посилання на state.users не змінилось, повертається закешований результатКомпонент ре-рендериться тільки коли масив users у store дійсно змінюється, а не при кожному dispatch.
Проміжний: todo-додаток з фільтром видимості
const getTodos = (state) => state.todos;
const getVisibilityFilter = (state) => state.visibilityFilter;
const getVisibleTodos = createSelector(
[getTodos, getVisibilityFilter],
(todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED': return todos.filter(t => t.completed);
case 'SHOW_ACTIVE': return todos.filter(t => !t.completed);
default: return todos;
}
}
);
// У компоненті
const todos = useSelector(getVisibleTodos);
// Перераховується тільки при зміні масиву todos або рядка filter
// Обидва inputs відстежуються незалежноЦей патерн з офіційних прикладів Redux показує навіщо потрібні selectors. Без getVisibleTodos кожен компонент дублював би логіку фільтрації або перезапускав її при кожному рендері незалежно від того що змінилось.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.