Почему они могут выполнять вычисления чаще, чем предполагалось, и некоторые передовые методы управления более сложными сценариями

Reselect - ключевой компонент любой архитектуры, использующей шаблон Redux. Он играет большую роль в доставке нужных данных каждому компоненту, а также помогает информировать React о том, когда нужно выполнить повторный рендеринг. Поэтому очень важно, чтобы селекторы были эффективными, они пересчитывали только те действия, которые производят новый результат.

К сожалению, очень легко сделать простые ошибки, которые могут привести к ненужным перерисовкам в вашем приложении. В этой статье я рассмотрю некоторые из этих распространенных проблем и расскажу, как создавать селекторы для максимальной эффективности.

Функция createSelector

Чтобы понять, как создавать хорошие селекторы, важно глубоко понять, как эта функция действительно работает. Для справки, вот подпись функции createSelector:

createSelector(…inputSelectors | [inputSelectors], resultFunc)

Начнем с параметра inputSelectors, который представляет собой просто список из одной или нескольких функций. Каждый из них выполняется каждый раз, когда вызывается селектор, и результаты которого сравниваются с результатами предыдущего вызова. Если набор значений обоих прогонов эквивалентен, тогда resultFunc не нужно будет запускать и может вернуть предыдущий результат.

Эта логика мемоизации Reselect (и других подобных реализаций, таких как селекторы NgRx) - это то, что вы всегда должны иметь в виду при разработке своих селекторов. Прежде чем мы поговорим о том, что может пойти не так с селекторами, следует отметить ключевое различие, основанное на типе предоставленного inputSelector(s).

«Корневые» селекторы

Еще одна концепция, которую следует знать, - это разница между селекторами, которые напрямую ссылаются на состояние, и теми, которые полагаются только на другие селекторы Reselect. Мне нравится называть прежние корневые селекторы, поскольку они как бы существуют на верхнем уровне вашего дерева селекторов. Вот простой пример:

За неимением лучшего термина давайте вызовем селекторы, у которых есть только «производные» селекторы Reselect input. Важно легко различать эти два элемента, потому что корневые селекторы имеют больше ограничений, которые я объясню более подробно позже.

Распространенные ошибки

Возврат сложных объектов в корень inputSelectors

Как правило, очень важно избегать возврата чего-либо, кроме примитивов, в корневые селекторы ввода. Давайте посмотрим на тривиальный пример этого:

createSelector(
  state => new Date(state.createdTimestamp), // unstable reference
  date => ({
    month: date.getMonth() + 1,
    year: date.getYear()
  })
);

Если ваш компонент зависит от этого селектора, вы можете заметить его повторную визуализацию (в зависимости от вашей структуры представления) каждый раз, когда отправляется действие, потому что new Date() каждый раз приводит к новой ссылке. В этом случае решение довольно простое:

createSelector(
  state => state.createdTimestamp, // Integer
  timestamp => ({
    month: new Date(timestamp).getMonth() + 1,
    year: new Date(timestamp).getYear()
  })
);

createdTimestamp - это примитив, и это то, что должно быть возвращено вместо него. Теперь ваш компонент будет повторно визуализироваться только в том случае, если createdTimestamp действительно вернет новую метку времени.

Еще одна простая ошибка - вернуть поддерево состояния вместо листового узла (например, вернуть state.user вместо state.user.firstName). Теперь это может быть нормально, если ваш селектор фактически зависит от каждой отдельной части информации во всем поддереве, но если это не так, вы можете в конечном итоге перерисовать компонент без причины. Я лично видел, как это происходило несколько раз (многие из которых я делал сам), и обычно это заканчивается бомбой замедленного действия, ожидающей в будущем какой-то странной проблемы с повторным рендерингом, когда вы меньше всего этого ожидаете.

Сложная или дорогая логика в селекторах входов

Селекторы ввода выполняются каждый при отправке действия. Надеюсь, в вашем приложении отправляется не слишком много действий, но иногда бывают случаи, когда это может происходить довольно часто (например, ввод текста). Тем не менее, всегда лучше избегать всего, что требует больших затрат для выполнения в ваших селекторах ввода, даже если они в конечном итоге возвращают только примитив. Если вы обнаружите, что хотите это сделать, вам следует создать отдельный селектор Reselect для вычисления этого значения и использовать его вместо этого в качестве селектора ввода.

Лучшие практики

Сохраняйте "корневые" селекторы ввода простыми

Вкратце, каждый селектор ввода должен просто выбрать один примитив (строку, число, логическое значение и т. Д.) Из состояния. Это гарантирует, что функция результата вашего селектора срабатывает только тогда, когда это абсолютно необходимо, что означает, что ваши нижестоящие компоненты (React или другие) также будут повторно визуализироваться только тогда, когда это необходимо.

Селекторы слоев для более сложных сценариев

Учитывая ограничение на возврат примитивов в корневых селекторах ввода, необходимо добавить два или более уровней селекторов, чтобы обойти это. Поскольку селекторы Reselect будут генерировать новое значение только при изменении входных данных, его выход можно рассматривать как примитив, когда он используется как вход для другого селектора. Это позволит вам получать столько сложных объектов, сколько вам нужно.

В приведенном выше примере chart выводит новый объект только при изменении любого из трех его входов. В результате chartState не будет лишних вычислений.

Будьте осторожны при использовании параметризованных селекторов

Если вы не знакомы, селекторы ввода могут принимать дополнительные аргументы, которые должны быть переданы при вызове вашего селектора.

const parameterized = createSelector(
  (state, id) => ... // additional id parameter
);
// In your component somewhere
parameterized(getState(), id); // id gets passed in here

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

Однако повторное использование параметризованного селектора в компоненте элемента списка было бы плохим вариантом. Поскольку каждый элемент списка будет вызывать селектор с новым идентификатором, мемоизация будет в основном бесполезной.

При необходимости вместо этого динамически создайте набор селекторов.

Обходной путь для отображения коллекции - динамически создать селектор для каждого компонента элемента списка. Таким образом, каждый компонент будет иметь соответствующий мемоизированный селектор. См. Следующий пример:

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

В цепочке компонентов попробуйте использовать селектор только один раз

Вероятно, это не повлияет на производительность, но рекомендуется не ссылаться на свои селекторы и не вызывать их больше, чем необходимо. Если селектор создается динамически, вы определенно захотите повторно использовать тот же самый экземпляр, насколько это возможно, чтобы избежать дублирования работы с мемоизацией.

Поскольку есть случаи, о которых упоминалось выше, когда это может быть проблемой, проще просто полностью избежать такой возможности, обращаясь к селектору как можно меньше раз (или только один раз). Если вы следуете практике минимизации количества интеллектуальных компонентов в своем приложении, это должно быть довольно просто.

Подводя итоги

Вот краткое изложение правил для справки:

  • Селекторы ввода корневого уровня всегда должны возвращать примитивы
  • Создавайте слои селекторов, если вам нужно, чтобы селектор ввода был сложным объектом.
  • Не используйте параметризованные селекторы в повторяющемся компоненте (например, элементе списка).
  • Создайте массив селекторов при рендеринге повторяющегося компонента, если вам нужно производное состояние

Потребуется немного практики, но если вы будете следовать этим правилам, у вас не должно возникнуть проблем с написанием эффективных селекторов. При этом я уверен, что есть еще много других крайних случаев, которые я пропустил. Если вы столкнетесь с чем-либо, основы, изложенные в этой статье, помогут вам их преодолеть.