Основные концепции
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); // true
is.unit(event); // true
is.unit(fx); // true
is.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));
// Если эффект выкинет ошибку, то мы отловим ее при помощи события fail
fetchUserFx.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",
},
]);
// Создали производные сторы, которые зависят от $supers
const $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 завершится успешно, то вызови getSupersFx
sample({
clock: saveNewSuperFx.done,
target: getSupersFx,
});
// вызываем загрузку данных
getSupersFx();
Вот так вот легко и незамысловато мы написали часть бизнес-логики нашего приложения, а часть с отображением этих данных оставили на UI фреймворк.
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.