Автор: Маслов Андрей, Front-end разработчик.
Время чтения: ~10 минут

Business logic with ease.

Содержание:

  1. О статье.

  2. Почему нужно использовать effector ?

  3. Концепция.

  4. Полезное и основное из api.

  5. Как работает ядро Effector, простым языком.

  6. Итоги.

  7. Полезные материалы.

О статье

Этой статьей я открываю туториал из последующих статей, посвященных 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.

Материалы для закрепления: