Введение в сущностное и производное состояние и способы их реализации с помощью Redux

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

Я проведу вас на простом примере с указанием различий и проиллюстрирую, почему так важно реализовать производное состояние с отдельным шаблоном. Во-первых, давайте перейдем к первому (и более простому) из двух типов - существенному состоянию.

Эссенциальное состояние

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

Производное состояние

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

Простой пример пошагового руководства

В этой статье, как и в остальных частях серии, я буду приводить примеры кода в основном в Redux. Это потому, что я считаю, что Redux / Flux является наиболее явным (ничего не делается автоматически) и дает отличные обучающие примеры. Большинство (или все?) Фреймворков управления состоянием в конечном итоге решают аналогичные проблемы, поэтому, если вы понимаете Flux, вы сможете легко понять и другие фреймворки.

Начнем с определения некоторого существенного состояния, управляемого редуктором. Я добавил несколько комментариев для тех, кто не знаком с Redux.

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

Обратите внимание, что в действиях UPDATE_FIRSTNAME и LOGIN оба должны выполнять одинаковую работу для вычисления свойства fullName. В этом простом случае это не такая уж большая проблема, но даже тогда мы должны преобразовать создание fullName в функцию. Кроме того, вы должны сослаться на существующее состояние в случае UPDATE_FIRSTNAME, который здесь не является блокировщиком, но если нужным вам состоянием управляет другой редуктор, вы застрянете. Со временем, по мере добавления более сложного состояния, производное состояние также станет более сложным, и поддержка редукторов станет проблемой. Давайте посмотрим на альтернативу, в которой вместо этого используется Reselect.

Теперь в этом случае у нас есть селектор getFullName, который автоматически пересчитывает fullName в любое время при обновлении firstName или lastName. Каждый раз, когда действие изменяет любое из свойств имени, селектор позаботится обо всем остальном, и нам не нужно беспокоиться об обновлении этого вручную, что будет намного безопаснее.

Повторно выберите мемоизацию

Что действительно связывает этот паттерн, так это мемоизация Reselect (большинство фреймворков должны иметь какую-либо форму мемоизации для производного состояния). Это очень просто, сохраняются только предыдущие значения входных функций и функции результата. Каждый раз, когда отправляется действие, как someAction в нашем примере, будут вызываться две функции ввода на getFullNameAndTime. Кроме того, это действие не приводит к изменению firstName или lastName, поэтому функции ввода будут возвращать значения, эквивалентные последнему. Поскольку входные значения эквивалентны, селектор пропустит вызов функции результата и просто вернет ссылку на предыдущее значение.

Эта мемоизация позволяет React знать, когда повторно отрисовывать соответствующий компонент, поэтому даже если наш селектор вернул вместо этого сложный объект, такой как { fullName }, он все равно перерисовал бы только тогда, когда это необходимо. Это будет важно, если вы в конечном итоге создадите иерархию селекторов, передавая сложные объекты из одного в другой. Я покажу это более подробно в одной из следующих статей. А пока давайте просто посмотрим, как работают функции ввода.

Упростите функции ввода

Селекторы Reselect (а также NgRx) должны выполнять свои функции ввода каждый раз, когда они вызываются. В Redux и подобных фреймворках вы можете предположить, что селекторы вызываются каждый раз при отправке действия. Это значит, что они должны быть максимально простыми. Вероятно, очень мало случаев (а может быть, совсем нет), когда необходимо сделать что-либо, кроме простого возврата значения из состояния. Кроме того, селекторы выполняют только простые === проверки на равенство, поэтому не создавайте функцию ввода, которая возвращает новый объект (state => { ... }) или что-то подобное, так как это всегда приведет к сбою проверки равенства и вызовет повторное вычисление.

Вам нужно будет проявлять гибкость время от времени

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

Вывод

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