React Front-end Инженер

React Front-end Инженер

Роадмап навыков для прокачки

Селекторы в Redux: умение объяснить своими словами

ReactReduxSelectors

Основная идея

Selector в Redux — это чистая функция, которая принимает глобальный state и возвращает из него нужный фрагмент данных. Селекторы служат единой точкой доступа к данным из store, инкапсулируя логику извлечения и трансформации состояния.

Ключевые аспекты

  • Инкапсуляция доступа — компоненты не знают структуру state, а обращаются через селекторы
  • Переиспользование — один селектор можно использовать в разных частях приложения
  • Мемоизация — библиотека Reselect кэширует результаты и пересчитывает только при изменении входных данных
  • Композиция — сложные селекторы строятся из простых
  • Тестируемость — чистые функции легко тестировать в изоляции

Плюсы

  • Уменьшают дублирование кода доступа к state
  • Упрощают рефакторинг структуры store
  • Оптимизируют производительность через мемоизацию
  • Делают код более декларативным и читаемым

Минусы

  • Дополнительный слой абстракции для простых случаев
  • Необходимость следить за правильной композицией для мемоизации
  • Увеличение количества файлов в проекте

Частые ошибки на собеседованиях

  • Путают selector с reducer — reducer изменяет state, selector только читает
  • Забывают про мемоизацию и создают новые объекты/массивы при каждом вызове
  • Не понимают разницу между обычной функцией и мемоизированным селектором
  • Создают селекторы с побочными эффектами, нарушая принцип чистой функции
  • Не используют композицию, дублируя логику в разных селекторах

Введение и проблематика

В Redux-приложениях состояние хранится в едином глобальном store. По мере роста приложения структура state становится сложной и глубоко вложенной. Компоненты начинают напрямую обращаться к конкретным путям в объекте состояния, что создаёт жёсткую связь между UI и структурой данных.

Какую проблему решают селекторы?

Без селекторов код компонента выглядит так:

Code Example 1: Какая проблема возникает, когда компоненты напрямую обращаются к структуре state?

tsx
// Компонент знает точную структуру state
const user = state.entities.users.byId[userId];
const posts = state.entities.posts.list.filter(p => p.authorId === userId);

Если структура state изменится, придётся править десятки компонентов. Селекторы решают эту проблему, выступая единственным местом, которое «знает» как устроен state.

Selector — это чистая функция, которая принимает state (и опционально props) и возвращает производное значение.


Базовая теория

Определение селектора

Селектор — это функция вида:

ts
type Selector<State, Result> = (state: State) => Result;

Простейший селектор извлекает часть state:

Code Example 2: Что такое input selector? Какие преимущества даёт использование селекторов?

ts
// Базовые селекторы (input selectors)
const selectUsers = (state: RootState) => state.users;
const selectPosts = (state: RootState) => state.posts;
const selectCurrentUserId = (state: RootState) => state.auth.currentUserId;

Терминология

ТерминОписание
Input SelectorПростой селектор, извлекающий часть state без трансформации
Output SelectorСелектор, комбинирующий результаты других селекторов
Memoized SelectorСелектор с кэшированием результата (Reselect)
Parameterized SelectorСелектор, принимающий дополнительные параметры

Ключевые принципы

  1. Чистота — селектор не должен иметь побочных эффектов
  2. Детерминированность — одинаковый вход всегда даёт одинаковый выход
  3. Инкапсуляция — компонент не знает структуру state
  4. Композиция — сложные селекторы строятся из простых

Практические примеры

Простые селекторы

Code Example 3: Как создать селектор с параметром? Объясните работу selectUserById.

ts
// selectors/userSelectors.ts
 
// Input selector — прямой доступ к части state
export const selectUsersState = (state: RootState) => state.users;
 
// Производный селектор
export const selectAllUsers = (state: RootState) =>
  Object.values(state.users.byId);
 
// Селектор с параметром
export const selectUserById = (state: RootState, userId: string) =>
  state.users.byId[userId];
 
// Селектор статуса загрузки
export const selectUsersLoading = (state: RootState) =>
  state.users.status === 'loading';

Использование в компонентах

Code Example 4: Как использовать селекторы с useSelector? Почему передаём функцию внутрь useSelector?

tsx
import { useSelector } from 'react-redux';
import { selectUserById, selectUsersLoading } from './selectors';
 
function UserProfile({ userId }: { userId: string }) {
  // Используем селектор с параметром
  const user = useSelector((state: RootState) =>
    selectUserById(state, userId)
  );
  const isLoading = useSelector(selectUsersLoading);
 
  if (isLoading) return <Spinner />;
  if (!user) return <NotFound />;
 
  return <div>{user.name}</div>;
}

Мемоизация с Reselect

Библиотека Reselect позволяет создавать мемоизированные селекторы:

Code Example 5: Зачем нужна мемоизация в селекторах? Когда selectCurrentUserPosts будет пересчитан?

ts
import { createSelector } from '@reduxjs/toolkit'; // или 'reselect'
 
// Input selectors
const selectPosts = (state: RootState) => state.posts.items;
const selectCurrentUserId = (state: RootState) => state.auth.userId;
 
// Мемоизированный селектор
// Пересчитывается только если posts или userId изменились
export const selectCurrentUserPosts = createSelector(
  [selectPosts, selectCurrentUserId],
  (posts, userId) => {
    console.log('Пересчёт селектора'); // вызовется только при изменении
    return posts.filter(post => post.authorId === userId);
  }
);
⚠️

Без мемоизации filter() создаёт новый массив при каждом рендере, вызывая лишние перерисовки компонентов.

Композиция селекторов

Code Example 6: Как работает композиция селекторов? Что происходит при изменении selectUsers?

ts
// Базовые селекторы
const selectUsers = (state: RootState) => state.users.byId;
const selectPosts = (state: RootState) => state.posts.items;
 
// Композитный селектор — посты с данными авторов
export const selectPostsWithAuthors = createSelector(
  [selectPosts, selectUsers],
  (posts, users) =>
    posts.map(post => ({
      ...post,
      author: users[post.authorId],
    }))
);
 
// Ещё один уровень композиции
export const selectPublishedPostsWithAuthors = createSelector(
  [selectPostsWithAuthors],
  (posts) => posts.filter(post => post.status === 'published')
);

Пограничные кейсы

Селекторы с параметрами и мемоизация

🚫

Стандартный createSelector кэширует только последний результат. Для параметризованных селекторов это проблема.

Code Example 7: Почему этот селектор неэффективен при вызове с разными userId? Как решить эту проблему?

ts
// ❌ Проблема: кэш сбрасывается при каждом новом userId
const selectUserById = createSelector(
  [selectUsers, (_, userId: string) => userId],
  (users, userId) => users[userId]
);
 
// При вызове с разными userId кэш постоянно сбрасывается:
// selectUserById(state, 'user1') // кэш: user1
// selectUserById(state, 'user2') // кэш сбросился, теперь: user2
// selectUserById(state, 'user1') // опять пересчёт!

Решения:

Code Example 8: Что такое фабрика селекторов? Зачем каждому компоненту нужен свой экземпляр?

ts
// ✅ Решение 1: Фабрика селекторов
const makeSelectUserById = () =>
  createSelector(
    [selectUsers, (_, userId: string) => userId],
    (users, userId) => users[userId]
  );
 
// Каждый компонент получает свой экземпляр
const selectUser = useMemo(makeSelectUserById, []);
 
// ✅ Решение 2: re-reselect для кэширования по ключу
import { createCachedSelector } from 're-reselect';
 
const selectUserById = createCachedSelector(
  [selectUsers, (_, userId: string) => userId],
  (users, userId) => users[userId]
)(
  (_, userId) => userId // ключ кэша
);

Селекторы и нормализованные данные

ts
// State с нормализованными данными
interface NormalizedState {
  users: {
    byId: Record<string, User>;
    allIds: string[];
  };
}
 
// Селектор для получения списка из нормализованной структуры
export const selectUsersList = createSelector(
  [(state: RootState) => state.users.byId,
   (state: RootState) => state.users.allIds],
  (byId, allIds) => allIds.map(id => byId[id])
);

Продвинутые аспекты

Типизация селекторов в TypeScript

ts
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from '../store';
 
// Типизированный селектор
export const selectFilteredTodos = createSelector(
  [(state: RootState) => state.todos.items,
   (state: RootState) => state.filters.status],
  (todos, status): Todo[] => {
    if (status === 'all') return todos;
    return todos.filter(todo => todo.status === status);
  }
);
 
// Вывод типа результата автоматический
type FilteredTodos = ReturnType<typeof selectFilteredTodos>; // Todo[]

Селекторы в Redux Toolkit

Redux Toolkit включает Reselect и рекомендует размещать селекторы рядом со slice:

ts
// features/todos/todosSlice.ts
import { createSlice, createSelector } from '@reduxjs/toolkit';
 
const todosSlice = createSlice({
  name: 'todos',
  initialState: { items: [], filter: 'all' },
  reducers: { /* ... */ },
});
 
// Селекторы экспортируются вместе со slice
export const selectTodosState = (state: RootState) => state.todos;
 
export const selectVisibleTodos = createSelector(
  [selectTodosState],
  (todosState) => {
    const { items, filter } = todosState;
    switch (filter) {
      case 'completed': return items.filter(t => t.completed);
      case 'active': return items.filter(t => !t.completed);
      default: return items;
    }
  }
);

Плюсы и минусы

АспектПлюсыМинусы
АбстракцияКомпоненты независимы от структуры stateДополнительный слой кода
ПереиспользованиеОдна логика — много компонентовНужно продумывать API селекторов
ПроизводительностьМемоизация предотвращает лишние рендерыНеправильная композиция ломает кэш
ТестированиеЧистые функции легко тестироватьНужны отдельные тесты для селекторов
РефакторингИзменение state в одном местеНачальные затраты на создание слоя

Вопросы интервьюера

Q: Чем selector отличается от reducer?

Reducer — чистая функция, которая принимает state и action и возвращает новый state (изменяет данные). Selector — чистая функция, которая принимает state и возвращает производное значение (только читает данные).

Q: Зачем нужна мемоизация в селекторах?

Без мемоизации селектор с .filter() или .map() создаёт новый массив при каждом вызове. React считает, что данные изменились, и перерисовывает компонент. Мемоизация возвращает тот же объект, если входные данные не изменились.

Q: Когда НЕ нужен createSelector?

Если селектор просто возвращает часть state без трансформации (state => state.users), мемоизация не нужна — ссылка и так стабильна.

Q: Как тестировать селекторы?

Селекторы — чистые функции. Передаём mock-state, проверяем результат. Для мемоизированных селекторов можно проверить selector.recomputations().


Источники

Code Example 1: Problem without selectors

❓ Какая проблема возникает, когда компоненты напрямую обращаются к структуре state?

tsx
const user = state.entities.users.byId[userId];
const posts = state.entities.posts.list.filter(p => p.authorId === userId);

Code Example 2: Basic selectors

❓ Что такое input selector? Какие преимущества даёт использование селекторов?

ts
const selectUsers = (state: RootState) => state.users;
const selectPosts = (state: RootState) => state.posts;
const selectCurrentUserId = (state: RootState) => state.auth.currentUserId;

Code Example 3: Selectors with parameters

❓ Как создать селектор с параметром? Объясните работу selectUserById.

ts
export const selectUsersState = (state: RootState) => state.users;
 
export const selectAllUsers = (state: RootState) =>
  Object.values(state.users.byId);
 
export const selectUserById = (state: RootState, userId: string) =>
  state.users.byId[userId];
 
export const selectUsersLoading = (state: RootState) =>
  state.users.status === 'loading';

Code Example 4: useSelector with selectors

❓ Как использовать селекторы с useSelector? Почему передаём функцию внутрь useSelector?

tsx
import { useSelector } from 'react-redux';
import { selectUserById, selectUsersLoading } from './selectors';
 
function UserProfile({ userId }: { userId: string }) {
  const user = useSelector((state: RootState) =>
    selectUserById(state, userId)
  );
  const isLoading = useSelector(selectUsersLoading);
 
  if (isLoading) return <Spinner />;
  if (!user) return <NotFound />;
 
  return <div>{user.name}</div>;
}

Code Example 5: Memoized selectors with createSelector

❓ Зачем нужна мемоизация в селекторах? Когда selectCurrentUserPosts будет пересчитан?

ts
import { createSelector } from '@reduxjs/toolkit';
 
const selectPosts = (state: RootState) => state.posts.items;
const selectCurrentUserId = (state: RootState) => state.auth.userId;
 
export const selectCurrentUserPosts = createSelector(
  [selectPosts, selectCurrentUserId],
  (posts, userId) => {
    console.log('Пересчёт селектора');
    return posts.filter(post => post.authorId === userId);
  }
);

Code Example 6: Selector composition

❓ Как работает композиция селекторов? Что происходит при изменении selectUsers?

ts
const selectUsers = (state: RootState) => state.users.byId;
const selectPosts = (state: RootState) => state.posts.items;
 
export const selectPostsWithAuthors = createSelector(
  [selectPosts, selectUsers],
  (posts, users) =>
    posts.map(post => ({
      ...post,
      author: users[post.authorId],
    }))
);
 
export const selectPublishedPostsWithAuthors = createSelector(
  [selectPostsWithAuthors],
  (posts) => posts.filter(post => post.status === 'published')
);

Code Example 7: Parameterized selectors problem

❓ Почему этот селектор неэффективен при вызове с разными userId? Как решить эту проблему?

ts
const selectUserById = createSelector(
  [selectUsers, (_, userId: string) => userId],
  (users, userId) => users[userId]
);

Code Example 8: Selector factory pattern

❓ Что такое фабрика селекторов? Зачем каждому компоненту нужен свой экземпляр?

ts
const makeSelectUserById = () =>
  createSelector(
    [selectUsers, (_, userId: string) => userId],
    (users, userId) => users[userId]
  );
 
const selectUser = useMemo(makeSelectUserById, []);