Автор: Маслов Андрей, Front-end разработчик.
Время чтения: ~10 минут
Business logic with ease.
Содержание:
О статье.
Почему нужно использовать effector ?
Концепция.
Полезное и основное из api.
Как работает ядро Effector, простым языком.
Итоги.
Полезные материалы.
О статье
Этой статьей я открываю туториал из последующих статей, посвященных Effector JS - не только удобному менеджеру состояний, но и мощнейшему из инструментов на сегодняшний день (по-моему личному мнению).
Часть №1 будет нести ознакомительный характер c инструментом, чтобы вы могли понять, нужен ли вам Effector или нет. Разберем основные возможности и затронем то, как работает ядро библиотеки.
Почему нужно использовать Effector ?
Больше никакого бойлерплейт-кода.
Приложение принимает изначально расширяемую архитектуру (т.к эффектор позволяет изолировать бизнес-логику по процессам, здесь же стоит сказать и об feature-sliced архитектуре, о которой мы поговорим в последующих статьях).
Удобное и большое API, которое избавит разработчика от многих рутинных вещей.
Бизнес-логика теперь не "размазана" по файлам-контроллерам, а изолирована по процессам, интерфейсы не пересекаются с логикой (подобие реализации MV* паттернов).
Никакой магии, все построено на графах и подписках (об этом поговорим в конце этой статьи).
Есть русскоязычное комьюнити, подробная документация на русском и английском языках.
Постоянная поддержка, релизы с фиксами и новыми фичами.
Легковесность и скорость.
Поддержка TypeScript.
Концепция
Работу всего стейт-менеджера обеспечивает три основных юнита:
Store
Объект для хранения данных.
createStore - функция создания стора, название принято начинать со знака $.
Event
Этот юнит является главенствующей управляющей сущностью. С помощью event запускаются реактивные вычисления в приложении.
createEvent - функция создания события.
Вы можете подписать ваш store на какие-либо эвенты, при которых все зависимые от этого стора компоненты будут обновляться, сделать это можно при помощи .on, передав в метод первым аргументом - юнит, вторым - коллбэк функцию, которая будет возвращать результат изменения стора.
Пример:
//init.js
export const eventPlus = createEvent()
export const eventMinus = createEvent()
export const $storeCounter = createStore(0)
.on(eventPlus, (store) => store + 1)
.on(eventMinus, (store) => store - 1)
//components.jsx
export Component = () => {
const count = useStore($storeCounter)
//От этого хука можно будет отказаться,
//при использовании effector/reflect (рассмотрим в последующих туториалах)
return (
<h1>{ count }</h1>
)
}
Согласитесь, выглядит очень лаконично и просто.
Effect
Этот юнит очень похож на предыдущий, за тем лишь исключением, что эффект создает цепочку событийных вызовов, состоящих по факту из event. Обычно этот юнит используется при работе с асинхронными функциями.
createEffect - функция создания эффекта, принимает в себя первым аргументов коллбэк функцию - обработчик вызова эффекта, название такой функции принято заканчивать на Fx.
Пример:
//api.js
export const getCount = (payload) => {
return axios.get('/count', payload)
}
//init.js
export const getCountFx = createEffect(getCount)
Effect предоставляет множество эвентов, например, doneData, failData, pending и тд. (Подробнее можно ознакомиться в документации).
Ниже приведу пример работы с эффектом и его возможности, сразу стоит сказать, что работа с variant из effector/reflect облегчит обработку состояний компонента, но как ранее упоминал, разберем в следующих статьях.
//init.js
export const $count = createStore(0)
.on(getCountFx.doneData, (_store, res) => res.data.count)
//components.jsx
export Component = () => {
const count = useStore($count)
if (getCountFx.pending) {
return <h1>Loading...</h1>
}
if (getCountFx.failData) {
return <h1>Error</h1>
}
return (
<h1>{ count }</h1>
)
}
Полезное и основное из api
combine - позволяет комбинировать несколько сторов и создавать один производный.
Создадим стор, который будет хранить булево значение дизейбла кнопки submit, если запрос на получение счетчика в статусе "pending" или если этот запрос завершился в блоке catch. Третий аргумент является необязательным и служит для трансформации состояния.
const $submitDisabled = combine(
getCountFx.pending,
getCountFx.failData,
(pending, faildData) => pending || faildData
)
forward - создает связь между юнитами, с которыми мы разобрались чуть выше.
Напишем код, который будет выводить ошибку.
Forward принимает объект с двумя полями: from и to, которые ожидают юниты (или массивы юнитов), при выполнении from вызовется юнит to.
const showErrorFx = createEffect(() => showToast('Something went wrong'))
forward({
from: getCountFx.failData,
to: showErrorFx
})
guard - метод, который позволяет запускать юниты по условию.
Напишем код, который будет отправлять запрос формы на бэкенд, если форма валидна.
guard({
clock: sendEvent, //юнит, при срабатывании которого будет выполняться filter
filter: $isValid, //дальнейший вызов target возможен при filter = true
source: $form, //данные, которые будут передаваться в target
target: submitFormFx // юнит, который будет вызван при вызове clock и истинном значении filter
})
sample - метод, принцип работы как у guard, добавляется аргумент fn - коллбэк, результат вызова которого будет передан в target.
sample({ source?, clock?, filter?, fn?, target?})
API Effector предоставляет большое разнообразие методов, выше вы видите лишь те, которые я использовал на проекте чаще остальных, и это малость из доступных, обязательно рассмотрите следующие методы: is, restore, split, attach. Так же почитайте про нерассмотренный ранее юнит domain.
Просто о том, как работает ядро Effector.
Основа - обход графа в ширину, где вершины графа являются событиями в очереди, которые хранятся в объекте ядра, выглядит так:
export type Node = {
id: ID
next: Array<Node>
seq: Array<Cmd>
scope: {[key: string]: any}
meta: {[tag: string]: any}
family: {
type: 'regular' | 'crosslink' | 'domainn'
links: Node[]
owners: Node[]
}
}
Рассмотрим три основных свойства объекта:
next - массив ребер графа (ссылки на следующие вершины)
seq - массив с последовательностью шагов
scope - объект данных, необходимый для работы шагов
При попадании данных в ядро (например, вызов event) - запускается цепочка событий, которая обновляет все связанные сторы, и собственно компоненты. При попадании ноды в очередь, у нас происходит выполнение шагов из seq (вычисления, фильтрация, перемещение ноды в режим ожидания, и другие, в материалах оставлю ссылку для полного ознакомления)
К слову, это упрощенная модель работы эффектора. Так, например, очередей в ядре 5, а не 1. Все эти очереди отличаются приоритетом к выполнению.
Итоги
При первом использовании данной библиотеки у меня было двоякое чувство, ведь зачем мне отказываться от того же Redux или MobX, "и так все хорошо", лишь после полугода плотного использования effector на проекте я стал замечать, что разрабатывать стало проще и быстрее, а код стал структурированным и понятным.
Хоть на просторе интернета и мало информации о разных решениях, чтобы новичку было легче вкатиться, все это перекрывается прекрасным комьюнити, которое всегда готово прийти на помощь, например, в tg канале. А этим циклом статей я попытаюсь сделать процесс погружения быстрым и легким.
Не стоит в комментариях заниматься холиварами, не воспринимайте статью, как единственный и истинный источник правды. Оставляйте комментарии по непоняткам, найдем правду вместе :D
В этой статье мы разобрали лишь мизерную часть того, что умеет effector, так что прошу пробежаться еще по материалам для закрепления.
Далее мы развернем реальное приложение в связке TypeScript + Effector (в документации все примеры на js, поэтому для новичков использование ts может стать не самым приятным делом), поговорим о feature-sliced архитектуре и best practices.