Основные концепции
Effector – это современная библиотека для работы с состоянием приложения, которая позволяет разработчикам создавать масштабируемые и предсказуемые реактивные приложения.
В основе Effector лежит концепция юнитов - независимых строительных блоков приложения. Каждый юнит: стор (store), событие (event) или эффект (effect), выполняет свою конкретную роль.
Объединяя эти юниты, разработчики могут создавать сложные, но понятные потоки данных в приложении.
Разработка с Effector строится на двух ключевых принципах:
- 📝 Декларативность: вы описываете что должно произойти, а не как это должно работать
- 🚀 Реактивность: изменения автоматически распространяются по всему приложению
Effector использует умную систему отслеживания зависимостей, которая гарантирует, что при изменении данных обновятся только действительно зависимые части приложения. Благодаря этому:
- Разработчикам не нужно вручную управлять подписками
- Производительность остается высокой даже при масштабировании
- Поток данных остается предсказуемым и понятным
Юниты
Юнит - это базовое понятие в Effector. Store, Event и Effect – это все юниты, то есть базовые строительные блоки для создания бизнес-логики приложения. Каждый юнит представляет собой независимую сущность, которая может быть:
- Связана с другими юнитами
- Подписана на изменения других юнитов
- Использована для создания новых юнитов
import { createStore, createEvent, createEffect, is } from "effector";
const $counter = createStore(0);const event = createEvent();const fx = createEffect(() => {});
// Проверка, является ли значение юнитомis.unit($counter); // trueis.unit(event); // trueis.unit(fx); // trueis.unit({}); // false
Событие
Событие (Event) — Событие в Effector представляет собой точку входа в реактивный поток данных, проще говоря это способ сказать приложению “что-то произошло”.
Особенности события
- Простота: События в Effector являются минималистичными и легко создаются с помощью createEvent.
- Композиция: Вы можете комбинировать события, фильтровать их, изменять данные и передавать их в другие обработчики или сторы.
import { createEvent } from "effector";
// Создаем событиеconst formSubmitted = createEvent();
// Подписываемся на событиеformSubmitted.watch(() => console.log("Форма отправлена!"));
formSubmitted();
// Вывод в консоль:// "Форма отправлена!"
Стор
Стор (Store) — это место, где живут данные вашего приложения. Он представляет собой реактивное значение, обеспечивающую строгий контроль над мутациями и потоком данных.
Особенности сторов
- У вас может быть столько сторов, сколько вам нужно
- Стор поддерживает реактивность — изменения автоматически распространяются на все подписанные компоненты
- Effector оптимизирует ререндеры компонентов, подписанных на сторы, минимизируя лишние обновления
- Данные в сторе иммутабельнные
- Здесь нет
setState
, изменение состояния происходит через события
import { createStore, createEvent } from "effector";
// Создаем событиеconst superAdded = createEvent();
// Создаем сторconst $supers = createStore([ { name: "Человек-паук", role: "hero", }, { name: "Зеленый гоблин", role: "villain", },]);
// Обновляем стор при срабатывании события$supers.on(superAdded, (supers, newSuper) => [...supers, newSuper]);
// Вызываем событиеsuperAdded({ name: "Носорог", role: "villain",});
Эффект
Эффект (Effect) — Эффекты предназначены для обработки побочных действий — то есть для взаимодействия с внешним миром, например с http запросами, или для работы с таймерами.
Особенности эффекта
- У эффекта есть встроенные состояния
pending
и событияdone
,fail
, которые облегчают отслеживание выполнения операций. - Логика, связанная с взаимодействием с внешним миром, вынесена за пределы основной логики приложения. Это упрощает тестирование и делает код более предсказуемым.
- Может быть как асинхронным, так и синхронным
import { createEffect } from "effector";
const fetchUserFx = createEffect(async (userId) => { const response = await fetch(`/api/user/${userId}`); return response.json();});
// Подписываемся на результат эффектаfetchUserFx.done.watch(({ result }) => console.log("Данные пользователя:", result));// Если эффект выкинет ошибку, то мы отловим ее при помощи события failfetchUserFx.fail.watch(({ error }) => console.log("Произошла ошибка! ", error));
// Запускаем эффектfetchUserFx(1);
Реактивность
Как мы говорили в самом начале effector основан на принципах реактивности, где изменения автоматически распространяются через приложение. При этом вместо императивного подхода, где вы явно указываете как и когда обновлять данные, вы декларативно описываете связи между различными частями приложения.
Как работает реактивность в Effector
Рассмотрим пример из части про сторы, где мы имеем стор с массивом суперлюдей. Допустим у нас появилось новое требование это выводить отдельно друг от друга героев и злодеев. Реализовать это будет очень просто при помощи производных сторов:
import { createStore, createEvent } from "effector";
// Создаем событиеconst superAdded = createEvent();
// Создаем сторconst $supers = createStore([ { name: "Человек-паук", role: "hero", }, { name: "Зеленый гоблин", role: "villain", },]);
// Создали производные сторы, которые зависят от $supersconst $superHeroes = $supers.map((supers) => supers.filter((sup) => sup.role === "hero"));const $superVillains = $supers.map((supers) => supers.filter((sup) => sup.role === "villain"));
// Обновляем стор при срабатывании события$supers.on(superAdded, (supers, newSuper) => [...supers, newSuper]);
// Добавляем супераsuperAdded({ name: "Носорог", role: "villain",});
В этом примере мы создали производные сторы $superHeroes
и $superVillains
, которые будут зависеть от оригинального $supers
. При этом изменяя оригинальный стор, у нас также будут изменяться и производные – это и есть реактивность!
Как это все работает вместе?
А теперь давайте посмотрим как все это работает вместе. Все наши концепции объединяются в мощный, реактивный поток данных:
- Событие инициирует изменения (например, нажатие кнопки).
- Эти изменения влияют на стор, обновляя состояние приложения.
- При необходимости, Эффекты выполняют побочные действия, такие как взаимодействие с сервером.
Для примера мы все также возьмем код выше с суперами, однако немного изменим его добавив эффект с загрузкой первоначальных данных, как и в реальных приложениях:
import { createStore, createEvent, createEffect } from "effector";
// определяем наши сторыconst $supers = createStore([]);const $superHeroes = $supers.map((supers) => supers.filter((sup) => sup.role === "hero"));const $superVillains = $supers.map((supers) => supers.filter((sup) => sup.role === "villain"));
// создаем событияconst superAdded = createEvent();
// создаем эффекты для получения данныхconst getSupersFx = createEffect(async () => { const res = await fetch("/server/api/supers"); if (!res.ok) { throw new Error("something went wrong"); } const data = await res.json(); return data;});
// создаем эффекты для получения данныхconst saveNewSuperFx = createEffect(async (newSuper) => { // симуляция сохранения нового супера await new Promise((res) => setTimeout(res, 1500)); return newSuper;});
// когда загрузка завершилась успешно, устанавливаем данные$supers.on(getSupersFx.done, ({ result }) => result);// добавляем нового супера$supers.on(superAdded, (supers, newSuper) => [...supers, newSuper]);
// вызываем загрузку данныхgetSupersFx();
Это рекомендации команды effector использовать $
для сторов и fx
для эффектов.
Более подробно об этом можно почитать здесь.
Связываем юниты в единый поток
Все что нам осталось сделать это как-то связать вызов события superAdded
и его сохранение saveNewSuperFx
, а также после успешного сохранения запросить свежие данные с сервера.
Здесь нам на помощь приходит метод sample
. Если юниты это строительные блоки, то sample
– это клей, который связывает ваши юниты вместе.
sample
является основным методом работы с юнитами, который позволяет декларативно запустить цепочку действий.
import { createStore, createEvent, createEffect, sample } from "effector";
const $supers = createStore([]);const $superHeroes = $supers.map((supers) => supers.filter((sup) => sup.role === "hero"));const $superVillains = $supers.map((supers) => supers.filter((sup) => sup.role === "villain"));
const superAdded = createEvent();
const getSupersFx = createEffect(async () => { const res = await fetch("/server/api/supers"); if (!res.ok) { throw new Error("something went wrong"); } const data = await res.json(); return data;});
const saveNewSuperFx = createEffect(async (newSuper) => { // симуляция сохранения нового супера await new Promise((res) => setTimeout(res, 1500)); return newSuper;});
$supers.on(getSupersFx.done, ({ result }) => result);$supers.on(superAdded, (supers, newSuper) => [...supers, newSuper]);
// здесь мы говорим, при запуске clock вызови target и передай туда данныеsample({ clock: superAdded, target: saveNewSuperFx,});
// когда эффект saveNewSuperFx завершится успешно, то вызови getSupersFxsample({ clock: saveNewSuperFx.done, target: getSupersFx,});
// вызываем загрузку данныхgetSupersFx();
Вот так вот легко и незамысловато мы написали часть бизнес-логики нашего приложения, а часть с отображением этих данных оставили на UI фреймворк.
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.