Как стать автором
Обновить

Effector — убийца Redux? Туториал с нуля. Часть 1

Время на прочтение5 мин
Количество просмотров30K

Автор: Маслов Андрей, 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.

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

Теги:
Хабы:
Всего голосов 6: ↑5 и ↓1+6
Комментарии79

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань