Если видео недоступно для просмотра, попробуйте выключить блокировщик рекламы.

Еще одна задача, которую решают контейнеры — оптимизация. Они автоматически отслеживают то, что возвращается из mapStateToProps, и если ничего не изменилось с предыдущего рендеринга, то перерисовки не будет. По сути, контейнер ведет себя как PureComponent. Из этого есть несколько следствий:

  • Нужно стараться передавать как можно меньше данных (но не меньше, чем нужно).
  • Нужно избегать изменений в mapStateToProps.

Но не забывайте, что сама функция mapStateToProps выполняется всегда, даже если данные не изменились. Это приводит к ненужным, и иногда тяжелым, вычислениям.

Последнее рассмотрим подробнее. Напомню наш код из практики:

const mapStateToProps = ({ tasks }) => {
  const props = {
    tasks: Object.values(tasks),
  };
  return props;
};

На первый взгляд в коде все нормально, но, на самом деле, Object.values создает каждый раз новый объект, даже если tasks остались прежними. А значит ни о какой эффективности не может быть и речи. Очевидным решением будет перенести эту логику внутрь компонента, но тогда теряется одно из главных преимуществ маппинга. Компоненты завязываются на структуру состояния и выполняют работу по подготовке данных, которая, кстати, начнет дублироваться.

Для решения этой задачи создан пакет reselect. Он позволяет создавать специальные функции "селекторы", которые выполняют мемоизацию результата. То есть если данные не поменялись, то и результат работы функции будет тем же самым значением или объектом в случае составных данных.

Отмечу, что reselect не связан ни с Redux, ни с React. Нет никакого слоя интеграции. Селекторы сами по себе и их легко использовать в контейнерах без конфигурации.

Перед тем как создать первый селектор, нужно написать функцию, которая принимает на вход состояние и возвращает нужный срез данных. В нашем случае используется функция getTasks. Затем с помощью функции createSelector создается селектор. В примере выше в функцию createSelector передается наша исходная функция и вторая функция, которая производит фильтрацию данных, полученных первой функцией.

Посмотрите на этот код:

const getTasks = state => Object.values(state.tasks);

const mapStateToProps = (state) => {
  const props = {
    tasks: getTasks(state),
  };
  return props;
};

В коде выше сразу две ошибки. Во-первых, функция, извлекающая getTask, не селектор, хотя без нее селектор не сделаешь. То есть никакой мемоизации не будет. Во-вторых, ни в коем случае нельзя делать преобразования данных на этом этапе. Именно эту функцию используют селекторы, чтобы узнать, а изменились ли данные. Если функция содержит обработку, то данные всегда будут новые и смысл селектора пропадает. Хотя технически в коде он останется.

import { createSelector } from 'reselect';

const getTasks = state => state.tasks;
const tasksSelector = createSelector(
  getTasks,
  tasks => Object.values(tasks),
);

const mapStateToProps = (state) => {
  const props = {
    tasks: tasksSelector(state),
  };
  return props;
};

Хорошая новость в том, что селекторы можно соединять:

import { createSelector } from 'reselect';

const getTasks = state => state.tasks;
const tasksSelector = createSelector(
  getTasks,
  tasks => Object.values(tasks),
);
const publishedTasksSelector = createSelector(
  tasksSelector,
  tasks => tasks.filter(t => t.state === 'published'),
);
const percentOfFinishedTasksSelector = createSelector(
  tasksSelector,
  publishedTasksSelector,
  (tasks, publishedTasks) => (publishedTasks.length / tasks.length) * 100,
)

const mapStateToProps = (state) => {
  const props = {
    tasks: tasksSelector(state),
    publishedTasks: publishedTasksSelector(state),
    percentOfFinishedTasks: percentOfFinishedTasksSelector(state),
  };
  return props;
};

Как это работает:

  • Селектор вызывает все переданные ему селекторы (которые в свою очередь делают тоже самое и так до самого дна) и собирает результаты их вызовов в массив results.
  • Селектор вызывает последнюю переданную функцию как f(...results). Другими словами, количество аргументов в последней переданной функции селектору равно количеству селекторов, переданных перед этой функцией.
  • То, что получилось, и есть результат, который вернет селектор (а заодно сохранит внутри).

Хотя по началу такая комбинаторика может пугать, в реальности селекторы очень простая вещь. Кроме мемоизации, они позволяют переиспользовать выборки в разных компонентах. В файловой системе рекомендуется размещать их по пути selectors/index.js.

Когда стоит использовать селекторы, а когда нет? Большинству приложений они не понадобятся. Фронтенд приложения редко оперируют большим количеством данных одновременно. Разнообразные списки или формы хранят в себя максимум сотни или тысячу элементов. Более того. Оптимизировать код имеет смысл только тогда, когда приложение уже начало тормозить и мы точно убедились, что проблема в пересчете внутри mapStateToProps. До этого момента, лучше про оптимизацию и селекторы не вспоминать.

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →