Effector beginner's guide [RU]
Yan1. Введение
В этой статье я отвечу на повторяющиеся вопросы и разрешу недопонимания по стейт-менеджеру effector.js в формате статьи.
2. Мотивация
Зачем вообще это нужно? Потому что это инструмент, который в действительности может помочь облегчить рутину фронтэндеров. Ведь можно будет забыть почти полностью о пропсах, об их типизации, о кусках логики внутри компонентов, заучивании десятка другого операторов, использовать прокси или декораторы и при этом получать самый мощный инструмент по организации датафлоу на рынке, предоставляющий лишь функции и объекты.
Единственная проблема получить доступное введение в технологию так как нужно несколько перестроить майндсет. Я полагаю, что нашел путь к более мягкому "въезду", поэтому собираю ультимативную инструкцию в этой серии постов.
3. Приложение это система
Да, это действительно важная деталь в понимании и зачем вообще вот это все.
Давайте по шагам попробуем дойти до этого тезиса:
1) Цельные ли приложения по своей природе?Да
2) Могут ли приложения быть поделены по определенному признаку? Да
3) По какому? Зоны ответственности
4) Соединены ли зоны ответственности между собой? Да, определенно, так как это части конкретного приложения. Более того, они взаимодействуют друг с другом
5) А что такое система? Множество связанных вещей(зон ответственности), которые взаимодействуют друг с другом
Вау! Всего-навсего 5 шагов и подвели к такому тезису. Брависсимо!
4. Возвращаемся к нашим баранам (эффекторам)
Я специально в первом посте выделил слово датафлоу. Так как оно ключевое, а не вот это закрепившееся сокращем стм. Это ведет к заблуждениям. Стейт это лишь элемент для построения бизнес-логики. А не всеобъемлющее целое.
Кстати, об элементах. Эффектор предоставляет четыре юнита, оперируя которыми вы сможете построить бизнес-логику любой сложности: вода, земля, огонь и воздух(шутка).
5. Юниты: Event
Первый и самый важный. Дело в том, что мы как фронтэндеры живем в событийно-ориентированном окружении (DOM ака верстка). При построении бизнес-логики веб-приложений(те которые рядом с DOMом) глупо было бы ориентироваться на иную модель. Даже при разговоре c бизнесом в лице менеджеров и выше нередко можно услышать фразы наподобие: "Пользователь заходит, и тут наша фича такая ХОБА". Неявным образом в данном разговоре подразумеваются события:
1) пользователь заходит
2) фича делает хоба
Определение события со словарика.
6. Юниты: Store
Ух, вот она жемчужина СТМ. Сторчик, Стореныш.
Объект для хранения значений. Необходимо задавать дефолтное значение(можно все кроме undefined). При прилете повторяющегося(эквивалентного предыдущему) значения не тригернет апдейт.
Обработчик для прилетевших событий представляет собой редьюсер (не мутируем текущий стейт), в случае возврата undefined в обработчике апдейт не тригернется
Учитывая предыдущий апроач с зонами ответственности, можно вывести следующую рекомендацию:
Зоны ответственности позволяют нам иметь как минимум один стор под своим патронажем. И нет, не поля одного большого объекта. Независимые легкие сторы для каждой зоны ответственности.
Комбинировать их в гигастор не составит труда при возникшей необходимости.
7: Юниты: Effect
Ключевой для понимания юнит.
Технически признаками эффекта являются (хотя бы один):
-влияние на окружение вне системы (запросы на сервер, локалсторадж)
- подверженность влиянию окружения (process.env)
Но, концептуально, если событие это штука, которая происходит всегда, то эффект же предоставляет конструкцию, которая позволит обрабатывать исключения(то есть отсутствие гарантии, что все действо внутри обработчика будет завершено с успехом)
Когда мы можем ловить исключения?
-запросы
-работы с локал сторадж
-работа с third-party API
-произвольный участок кода, где разработчику жуть как хочется написать явный throw
Эффект предоставляет нам handler, в который будут складироваться все подобные сомнительные конструкции.
Таким образом, "прожевывая" сомнительные конструкции, эффект эмитит события об успехе(.done) или о несварении (.fail). Во время работы так же доступно булево поле-стор .pending, которое явно скажет о том в процессе эффект или нет.
Для тех кому все равно на исход любезно предоставлено событие .finally, которое эмитится всегда.
8. Стандартные юниты
Все вышеобозначенные три юнита являются стандартными. Это важное уточнение так как дальше этот термин будет использоваться для краткости.
P.S. на этот термин я постоянно буду ссылаться
9. Юниты: Domain
Домен это неймспейс для всех стандартных юнитов. Предоставляет хуки на создание стандартных юнитов, находящихся под патронажем этого домена. Что полезно для массовых операций. Домен может быть свободно создан внутри домена. Все юниты внутри домена могут выведены через domain.history
P.S. домены необходимы при SSR, а также при написании тестов, покрывающих большинство сценариев нашей системы.
10. Подготовка данных
Подобно караванам с товарами ивенты распространяют данные по нашей системе(только грабить их не нужно). Периодически нам нужно эти данные подготовить: добавить в эти данные какое-нибудь статическое значение или умножить пришедшую в данных цифру на два.
Для таких задач служат три вещи:
1) пожалуй самой «плоской» версией для подготовки данных между стандртным юнитом «отправки» и стандартным юнитом «назначения» (датафлоу все-таки) является поле fn в операторе sample. Но к нему вернусь через пару глав, так как обо всем по порядку.
2) остальные варианты являются методами события непосредственно. Первый из них event.map позволяет трансформировать payload, пришедший в событие как вам заблагорассудится лишь с одним ограничением: функция-трансформер должна быть чистой (то есть не содержать сайд-эффектов). Данный метод события вернет новое событие, которое будет неразрывно связано с оригинальным незамедлительным вызовом, как только оригинальное было «тригернуто».
3) и последний вариант это event.prepend . Если с .map мы взаимодействуем как с постобработчиком, то .prepend, напротив, будет являться прелюдией к оригинальному событию. Соответственно, возвращать будет событие которое исполнит функцию-трансформер и затем незамедлительно вызовет оригинальное событие. Какое можно найти этому применение?
Например, эффект по получению баланса определенной валюты. Обработчик для всех валют одинаковый, отличие будет будет лишь в статическом коде валюты. Таким образом, можно создать множество «препенднутых» событий, функция-трансформер которого примиксовывает в аргумента вызова статический код валюты и решить поставленную задачу.
11. Подготовка данных стора в необходимой форме
Данные из сторов тоже не последние люди в нашем цирке под названием датафлоу, поэтому как-то несправедливо было бы обделить стор методом для подготовки данных. Стор подобно событию имеет метод .map, в котором можно трасформировать стор по заранее указанным правилам. Такой стор называется вычисляемым стором.
Применение? Например, стор вам нужен в форме ассоциативного массива(ключ-значение) и в форме обычного массива объектов.
12. Датафлоу. Начало
Мы успели затронуть как обрабатывать данные в рамках одного стандартного юнита. А как же быть когда их несколько??
Вот тут-то и начинается самое интересное - декларативная связь юнитов! Первым самым простым оператором является оператор forward. Его апи достаточно явное: поля from и to, принимающие любой стандартный юнит. Его исполнение означает, что поле to явно подписано на триггер(изменение значения в сторе или вызов события) поля from и будет тригернуто соответственно после.
P. S. Только не надо подписывать завершение эффекта на его вызов - в вас ударит молния.
13. Датафлоу. Фильтрация
Обработка данных у нас есть, принуждение к общению юнитов - тоже. А что если юнитов не хотят общаться без соблюдения некоторых формальностей? Тут на помощь приходит guard. Оператор, имеющий три поля: source, filter, target.
Source это стандартный юнит, который инициирует общение.
Filter - та самая преграда в их общении. Принимает либо функцию-предикат, которая проверит данные, пришедшие из source на вшивость. Стоит этой функции вернуть truthly, все преграды уйдут. Помимо функции предиката может принимать булевый стор.
Target - стандартный юнит, получающий в себя данные из source, как только filter оказался truthly.
Но что, если одной фильтрации недостаточно и нужно в случае truthly еще и трансформировать payload неким образом? Тут придет на помощь event.filterMap
Так ладно это все круто, но ты рассматриваешь связи юнитов 1 к 1, а что если одному событию нужно связаться со многими событиями с различными условиями в зависимости от получателя?
И тут рецепт есть! Оператор split к вашим услугам.
14. Датафлоу. Сигналы
Частый случай, когда юнитам нужно связаться не просто так и даже не по условию, а по сигналу! А если точнее, то по тригеру любого стандартного юнита.
Самый явный пример - по маунту компонента(а маунт внезапно событие) взять данные из некого стора и вызвать эффект.
sample({
source: $store,
clock: mount,
target: effectFx
})
Это что-то на клингонском? Нет, это реализация задачи, которую я описал выше!
clock - здесь ключевое поле. В него и записывается необходимый сигнал для замыкания всей цепочки.
Как я обещал в 9-ой главе мы вернемся к способу подготовки данных через sample.
Дело в том, что помимо этих трех полей в sample существует опциональное поле fn - функция-комбинатор. Она принимает в себя два аргумента. payload из source и payload из clock (если не имеется - undefined). Далее мы вольны комбинировать и трансформировать эти значения в соответствии с поставленной задачей, не выходя за рамки чистоты этой функции, разумеется.
15. Организация датафлоу
Мы научились строить маршруты данных любой сложности по системе. Остался вопрос в организации этого дела. Я предлагаю максимально простой и наивный вариант - разделение на зоны ответственности.
Соответственно у нас есть папка со всей бизнес-логикой. Она разделяется на папки с соответствующими зонами ответствености.
Каждая зона ответственности содержит 2 файла (реже 3, когда сторы лежат в отдельном файле).
Первый - index-ный файл с декларациями всех юнитов эффектора(create***).
Второй - init файл, который ничего из себя не экспортит, а только импортит. Контент у этого файла следующего содержания:
1) хэндлеры эффектов
2) обработчики сторов соответствующей зоны ответственности
3) взаимодействие между юнитами из соседних зон ответственности(forward, guard, split, sample). когда задумываетесь в какой зоне ответственности разместить связь, просто задайте себе вопрос: «Кто инициатор этой связи?». Там и размещайте.
Так, в корне папки со всей бизнес-логикой создаем корневой init-файл, импортим в него init файлы со всех зон ответственности. Далее импортим этот корневой инит в корень приложения и инициализируем таким образом граф связей всего приложения статически!
Мы построили граф? Получается, что так.
P. S. Если чувствуете, что файлы зон ответственности начинают сильно разрастаться, это не подход плохой, а скорее вы упустили момент, когда зона ответственности превратилась в несколько.
16. Реиспользование и зависимый от окружения код
Периодически возникают ситуации, когда мы можем реиспользовать какие-то функции для нашего датафлоу или даже события для нескольких зон ответственности.
У нас есть зона ответственности app! Точно такая же как и остальные, хранит специфичный для зоны ответственности под названием приложение код.
Такая же история и с биндингами. Биндинги для реакта предоставляют такую штуку как Gate. Где их создавать? В определенной зоне ответственности или во вьюхе?
Их стоит создавать в зоне ответственности под названием приложение. Так как это специфичный код для конкретного приложения.
Та же история с init файлом. Те связи, где тригер гейта (маунт, анмаунт компонента или ререндер компонента, где у гейта обновились свойства) является инициатором, стоит размещать там.
Таким образом при тестировании будет явно видно какие события надо будет вызывать явно (вью-слоя по типу реакта в тестах бизнес-логики нет).
17. Тестирование
Я специально использовал словосочетание зоны ответственности вместо короткого слова домен, чтобы не сбивать вас с толку. Так как домен это юнит эффектора.
Говоря о тестировании бизнес-логики с нормальным покрытием, а не одиночными тестами, домен становится необходим.
1) Мы, как разработчики, можем создать один домен на всю систему.
2) Заменить явные импорты createEvent, createStore, createEffect на myDomain.createEvent и тд. Таким образом вся система становится под патронажем одного домена и его можно форкнуть - fork(domain, config)
3) Эта функция принимает в себя домен и опциональный конфиг, в котором вы можете явно указать хэндлеры каких эффектов вы хотите мокнуть через ключ handlers, а также явно указать значения сторов для тестов через ключ values
4) Вызов функции форк вернет вам скоуп( const scope = fork(domain, config) ) - виртуальный инстанс вашего домена
5) Теперь остается лишь выбрать начальное событие сценария, который мы хотим оттестировать, передав его в функцию allSettled первым аргументом, а вторым перед payload с которым этот сценарий должен начаться. В виду того, что резолв всей сценарной цепочки может занять больше времени чем один тик, вызов allSettled нужно await-ить
6) Через scope.getState($сторкоторыйнужен) проверяем состояния нашей системы по прошествии тестируемого сценария, вероятно, проверяем вызовы событий/эффектов нашей библиотекой для тестирования(например jest)
7) Вы способны оттестировать всю вашу систему!
18. SSR
Помните писал в позапрошлой главе о специфичном для окружения коде и его отделении? SSR ровно как и тесты подподает под это определение. Значит, нам нужно точно также форкнуть корневой домен и через allSettled вызвать все необходимые событийные махинации, которые мы задумали вычислить на сервере.
Но это лишь малюсенькая затравка для большой темы, которую @sovasergey раскроет в ближайшем будущем вдоль и поперек! Стей тунец.
19. Практическое применение(без SSR)
Я думаю без практических примеров вам было сложновато это воспринимать. Для таких целей в конце лета я сделал целое приложение-воркшоп для Odessa.js и всех желающих. Оно разбито по веткам. В мастере бойлерплейт, а дальше по главам можете навигироваться, заглядывая в пул реквесты, осматривая что изменилось.
Заключение
Я дал вполне исчерпывающую инфу по тому как стоит входить, это копипаст постов из канала https://t.me/effector_in_text
Далее, планирую еще сделать небольшой пост по тому как эффектор позволяет избавится от шаблонного кода, что тоже сильно облегчит нашу боль. Но, пожалуй, на первое время вам хватит и этого объема инфы для переваривания.
Стей тунец[2]