Лучшие состояния с TypeScript

Этот пост был впервые опубликован в блоге TK.

Это часть моих исследований по созданию устойчивого и последовательного программного обеспечения.

В этом посте мы поговорим об управлении состоянием в контексте React и Redux. Я покажу вам проблему, которую я пытался решить, и предлагаемое решение, которое я сделал для работы.

Эта проблема

Перво-наперво: проблема! Эта часть действительно важна. Я пытался решить проблему, а не добавлять блестящие технологии в наши PWA.

При разработке нового списка Originals мы извлекли данные из API для визуализации страницы. Важной частью этих данных был атрибут specialConditions. Это список условий недвижимости.

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

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

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

Итак, пришло в голову напечатать весь жизненный цикл Redux. Если мы введем состояние и каждый «агент жизненного цикла» Redux, мы сможем сделать его устойчивым и согласованным.

Решение

Идея заключалась в том, чтобы сделать архитектуру PWA явной, устойчивой и согласованной на протяжении всего жизненного цикла Redux путем ввода состояния, действий, редукторов, обработчиков и селекторов.

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

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

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

Предложение

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

  • Выбираем инструмент.
  • Агенты жизненного цикла Redux.
  • Неизменяемые данные.
  • Подтверждение концепции с помощью одного из наших PWA.
  • За и против.

Выбор инструмента

При выборе инструмента возникли вопросы:

  • Это действительно решает проблему?
  • Принятие на работе.
  • Инструмент в техническом сообществе.

Некоторые инструменты, которые могут решить проблему: Flow, ReasonML, Elm, ClojureScript и TypeScript. (Отказ от ответственности: ClojureScript принадлежит к семейству LISP. У него нет системы статических типов. Но у него есть некоторые интересные функции, такие как неизменяемые структуры данных.)

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

Flow и TypeScript, напротив, являются оболочками или надмножеством JavaScript. Изучать новые API легче, чем изучать совершенно новый язык. Несмотря на то, что нам нравится учиться и пробовать новое, я подумал, что нам нужно научиться плавно и по-прежнему решать главную проблему.

TypeScript используется в некоторых PWA. Некоторые используют для ввода API и контракта данных приложения с моделями TypeScript (классы, интерфейсы, типы). Другие используют сборщик данных для домов, поиска и окрестностей. Flow, напротив, не используется в наших PWA.

TypeScript - один из самых быстрорастущих языков и в настоящее время ведущий язык для компиляции в JavaScript. Некоторые крупные компании, такие как Airbnb, также масштабно внедряют этот инструмент ».

Итак, мы начали с TypeScript, чтобы проверить концепцию и посмотреть, как все пойдет.

Агенты жизненного цикла Redux

Идея предложения состоит в том, чтобы набрать агентов Redux. Для почти всех наших PWA у нас есть действия, редукторы, обработчики и селекторы для ввода.

  • Действия: использование типов для ввода контракта действий - тип, обещание, мета, extraProperties, свойства и т. Д.
  • Состояние хранилища: заключите контракт на initialState и сделайте его согласованным на протяжении всего жизненного цикла Redux.
  • Редукторы: позаботьтесь о государственном контракте, возвращая только контракт правильного типа - изменяя только данные, а не типы - с помощью обработчиков.
  • Обработчики: позаботьтесь о жизненном цикле внешнего взаимодействия и отображении состояний. Обеспечьте, чтобы в конечном состоянии был такой же контракт, как и ожидалось - контракт состояния магазина. Обработчики - обычное дело при использовании redux-pack.
  • Из данных внешнего взаимодействия: контракт на данные из API или Firestore или любое другое внешнее взаимодействие.
  • Чтобы сохранить состояние: используйте контракт состояния хранилища - в основном, контракт начального состояния редуктора.
  • Селекторы: получите состояние хранилища и сопоставьте его с состоянием компонента - реквизитами - с помощью mapStateToProps.
  • Типы: репозиторий типов для всех агентов.

Мы можем организовать эти агенты внутри папки компонентов контейнера:

__ containers
    	|__ MyComponent
    		|__ actions.ts
    		|__ handlers.ts
    		|__ reducer.ts
    		|__ selectors.ts
    		|__ types.ts

Неизменяемые данные

Immutable.js

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

В настоящее время мы используем Immutable.js для создания JavaScript с неизменяемыми структурами данных. Он дает JavaScript новые структуры данных для обработки неизменяемых данных с помощью совершенно нового API.

Предсказуемость действительно важна для понимания кода. Но Immutable.js не заставляет нас всегда использовать его в состоянии, поэтому мы не знаем, какой API использовать - Immutable или JavaScript API - например, для получения данных в селекторе.

В магазине легко смешивать данные. Частично это неизменяемый объект. Другой - это объекты Vanilla JavaScript.

Документация Redux вызвала некоторые опасения по поводу использования Immutable.js. А авторы Redux предлагают избегать использования Immutable.js с Redux. Для неизменяемых данных настоятельно рекомендуют использовать Immer.js.

Чтобы сделать его согласованным и предсказуемым, что, если мы будем обрабатывать неизменяемые данные во время компиляции и в процессе разработки с помощью lint и использовать только один языковой API, без необходимости рассуждать о языках - JavaScript и Immutable.js?

Машинопись только для чтения и TSLint-Immutable

Типскрипт имеет свойства только для чтения для обработки неизменяемых данных во время компиляции. Они есть:

  • readonly: неизменяемые примитивные данные
  • Readonly: неизменяемый объект
  • ReadonlyArray: неизменяемый массив

только для чтения

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

Если мы добавим эту функцию в определение типа, мы заставим данные быть неизменными во время компиляции. Если вы используете VS Code, в вашем коде будет ошибка “Cannot assign to ‘your property here’ because it is a read-only property”.

Только чтение

Добавьте неизменяемые функции для объектов.

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

ReadonlyArray

Добавьте неизменяемые функции для списков.

Если вы попытаетесь добавить новые элементы в массив только для чтения, вы получите ошибку “Property ‘push’ does not exist on type ‘readonly Readonly<T>[]”.

Тест: Immutable.js против нативных API

Мы сделали несколько тестов для сравнения PWA с Immutable.js и без него.

В первом тесте мы решили сравнить собственные API-интерфейсы JavaScript и Immutable.js: получение, получение, установка и установка. И поймите, как выглядит преобразование структуры данных с помощью функций fromJS и toJS.

Get - объект и массив

Получение первого атрибута объекта намного дороже для API Immutable.js. Семь раз (в миллисекундах) выполняется один миллион циклов и пять миллионов циклов. Получение первого элемента массива ближе по сравнению с этими API.

Get-in - объект и массив

Получение вложенного атрибута для объекта или вложенного элемента массива для API Immutable.js намного дороже, чем для собственного. И для одного, и для пяти миллионов циклов.

Набор - объект и массив

Установка нового значения для атрибута объекта намного дороже для собственного JavaScript API.

Но, используя метод set, мы по-прежнему можем работать с собственными объектами и значительно уменьшить миллисекунды. Для массива это ближе, но может быть лучше с методом set.

Набор - объект и массив

И для объектов, и для массивов лучше использовать собственный JavaScript API вместо структур и методов данных Immutable.js.

fromJS и toJS

Мы видим, что функция fromJS может быть дорогостоящей при преобразовании собственных структур данных JavaScript в Immutable DS.

toJS намного дешевле, но каждый раз, когда нам нужно получить значение, например, в селекторе, мы используем toJS, и это может повлиять на производительность.

Типизированное управление состоянием: жизненный цикл

Компонент визуализируется с начальным состоянием, определенным в редукторе. Он отправляет действие. Это действие будет иметь внешнее взаимодействие, поэтому оно использует конкурентную модель обещания.

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

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

От начального состояния до обновленного состояния после внешнего взаимодействия он имеет тот же тип контракта.

Типизированное состояние управления PoC

Использование этой концепции в качестве доказательства концепции Photos PWA: Photos PWA - это небольшое приложение, поддерживаемое небольшой командой, поэтому мы выбрали его как часть PoC. Нам нужно было проверить эту идею на производстве, но без особых сложностей.

Компонент контейнера, который мы применили к этой концепции, называется NotStartedPhotoSessions. Он получает конечную точку API, чтобы включить фотосессии в повестку дня фотографа.

Первым делом нужно было построить государственный контракт магазина в качестве начального состояния:

Итак, при первом рендеринге компонент будет обращаться к этим значениям как к свойствам. При рендеринге он отправит новое типизированное действие:

Редуктор получит это действие. И убедитесь, что конечный результат будет иметь тот же государственный контракт, что и исходное состояние:

Затем редуктор вызывает обработчик и преобразует полезную нагрузку ответа API в состояние хранилища. Это просто отображение данных.

Теперь, когда состояние хранилища обновлено, пора позволить селектору получить новые данные:

И мы возвращаемся к компоненту, где сопоставляем состояние с реквизитами и получаем новые данные.

Преимущества

  • Предсказуемость: проверка типов делает код более предсказуемым и, в свою очередь, делает его менее подверженным ошибкам.
  • Документация: заключение контрактов для каждого агента в жизненном цикле Redux дает нам хорошую документацию о них бесплатно.
  • Безопасность типов для потока данных: поскольку большая часть нашего потока данных происходит в жизненном цикле Redux, мы обеспечиваем безопасность типов, по крайней мере, во время компиляции, для наших данных, откуда происходит большинство наших ошибок.
  • Если мы решим удалить Immutable.js (fromJS и toJS) из состояния хранилища, мы все равно сможем использовать классные функции, такие как mergeDeep, без Immutable map / array / DS, но только используя Immutable.js версии 4.
  • Оптимизация производительности при удалении Immutable в пользу TypeScript только для чтения.
  • Неизменяемость по сравнению с тестом JS: получение, получение, установка, установка, fromJS, toJS.
  • Google Lighthouse: небольшое улучшение при запуске Google Lighthouse без Immutable.js.

Ресурсы