Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке selectors в Redux?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Selectors у Redux** є чистими функціями що зчитують дані зі стану store. Простий selector запускається на кожному рендері; `createSelector` з Reselect кешує результат і пропускає обчислення якщо вхідні дані не змінились. Використовуй Reselect для будь-яких похідних даних.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Selectors у Redux** є чистими функціями, які зчитують конкретні дані зі стану store і за потреби трансформують їх перед тим, як компонент їх отримує. ## Теорія ### TL;DR - Selector: функція що приймає `state` і повертає його частину - Прості selectors обчислюються на кожному рендері; мемоїзовані (Reselect) пропускають роботу якщо вхідні дані не змінились - `createSelector` варто використовувати для будь-яких похідних даних: відфільтрованих списків, обчислених сум, сортування - Аналогія: меню в ресторані, де ти замовляєш саме те що потрібно, а не весь склад кухні ### Швидкий приклад ```javascript // Простий 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** ```javascript // Неправильно - порушує чистоту і перевірку рівності за посиланням const getProcessed = createSelector([getUsers], (users) => { users[0].name = 'mutated'; return users; }); // Правильно - повертай нове посилання const getProcessed = createSelector([getUsers], (users) => users.map(u => ({ ...u, processed: true })) ); ``` Мутація ламає перевірку рівності за посиланням, на якій тримається мемоїзація. Результат: застарілі дані або пропущені ре-рендери. **2. Пропущені вхідні selectors** ```javascript // Неправильно - getPosts обходить кеш повністю createSelector([getUsers], getPosts, (users, posts) => ...); // Правильно - вказуй всі inputs у масиві createSelector([getUsers, getPosts], (users, posts) => ...); ``` Reselect відстежує тільки inputs перераховані в масиві. Решта передаються як аргументи але не спостерігаються. **3. Створення selector всередині компонента** ```javascript // Неправильно - нова інстанція 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** ```javascript // Може спричиняти зайві ре-рендери якщо 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-запитами, наприклад фільтрація або сортування вже закешованих даних. ## Приклади ### Базовий: фільтрація активних користувачів ```javascript 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-додаток з фільтром видимості ```javascript 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` кожен компонент дублював би логіку фільтрації або перезапускав її при кожному рендері незалежно від того що змінилось.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.