Перейти к содержимому

66. Effector: Effects

Асинхронность — это боль в большинстве state managers. Redux требует thunk/saga. MobX требует flow или action-декораторов. А в Effector есть Effect — первоклассный гражданин, который берёт всю async-логику на себя! 🚀

Представь Effect как «умный кнопочный выключатель»: нажал → он сам отслеживает, идёт ли ток (pending), пришло ли питание (done) или произошло короткое замыкание (fail).

import { createEffect } from 'effector';
// Базовый синтаксис: createEffect<Params, Result, Error>
const fetchUserFx = createEffect<number, User, Error>(async (userId) => {
const res = await fetch(\`/api/users/\${userId}\`);
if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
return res.json() as Promise<User>;
});
// Вызов эффекта — просто функция!
fetchUserFx(42); // запускаем с userId = 42

Каждый эффект автоматически создаёт набор производных юнитов:

const fetchUserFx = createEffect(async (id: number) => {
return await api.getUser(id);
});
// 📊 Stores (реактивные значения):
fetchUserFx.pending; // Store<boolean> — true пока выполняется
fetchUserFx.inFlight; // Store<number> — количество активных вызовов
// 🎯 Events (срабатывают при завершении):
fetchUserFx.done; // Event<{ params: number, result: User }>
fetchUserFx.fail; // Event<{ params: number, error: Error }>
fetchUserFx.finally; // Event<{ params, status: 'done'|'fail', ... }>
// 🔗 Shorthand events (только данные, без params):
fetchUserFx.doneData; // Event<User> — только результат
fetchUserFx.failData; // Event<Error> — только ошибка
import { createStore, createEffect, combine } from 'effector';
interface User {
id: number;
name: string;
email: string;
}
// Effect
const fetchUserFx = createEffect(async (id: number): Promise<User> => {
const res = await fetch(\`/api/users/\${id}\`);
if (!res.ok) throw new Error(\`Ошибка \${res.status}\`);
return res.json();
});
// Stores
const $user = createStore<User | null>(null);
const $error = createStore<string | null>(null);
// Подписки: обновляем сторы при завершении эффекта
$user
.on(fetchUserFx.doneData, (_, user) => user) // успех → сохраняем
.reset(fetchUserFx); // сбрасываем при новом запросе
$error
.on(fetchUserFx.failData, (_, err) => err.message) // ошибка → сохраняем
.reset(fetchUserFx); // сбрасываем при новом запросе
// Готовый combined store
const $pageState = combine({
user: $user,
error: $error,
isLoading: fetchUserFx.pending,
});
// Вызов
fetchUserFx(1);
// fetchUserFx.pending => true
// ... (ждём)
// fetchUserFx.doneData => { id: 1, name: 'Яша', email: '...' }
// fetchUserFx.pending => false
const loadItemFx = createEffect(async (id: number) => {
await delay(1000);
return { id, data: 'result' };
});
// inFlight считает активные вызовы
loadItemFx.inFlight.watch((count) => {
console.log(\`Активных запросов: \${count}\`);
});
// Запускаем несколько параллельно
loadItemFx(1);
loadItemFx(2);
loadItemFx(3);
// inFlight => 3
// После завершения всех => 0

sample — один из самых важных операторов Effector. Он позволяет «забрать» значение стора в момент срабатывания события:

import { createStore, createEvent, createEffect, sample } from 'effector';
const searchClicked = createEvent();
const $query = createStore('');
const searchFx = createEffect(async (query: string) => {
const res = await fetch(\`/api/search?q=\${query}\`);
return res.json();
});
// sample: когда searchClicked → взять $query → запустить searchFx
sample({
clock: searchClicked, // триггер
source: $query, // откуда берём данные
target: searchFx, // куда отправляем
});
// Теперь при searchClicked() автоматически вызовется searchFx с текущим значением $query!
import { guard, sample } from 'effector';
const $isLoggedIn = createStore(false);
const profileClicked = createEvent();
// Устаревший способ (guard):
guard({
source: profileClicked,
filter: $isLoggedIn, // выполнять только если $isLoggedIn === true
target: fetchProfileFx,
});
// Современный способ (sample с filter):
sample({
clock: profileClicked,
filter: $isLoggedIn,
target: fetchProfileFx,
});
import { forward } from 'effector';
// Устаревший способ:
forward({
from: buttonClicked,
to: someEffect,
});
// Современный способ (sample без source):
sample({
clock: buttonClicked,
target: someEffect,
});
import { createStore, createEvent, createEffect, sample, restore } from 'effector';
import { debounce } from 'patronum'; // утилиты для Effector
interface SearchResult { id: number; title: string; }
// Events и Effects
const queryChanged = createEvent<string>();
const searchFx = createEffect(async (query: string): Promise<SearchResult[]> => {
if (!query.trim()) return [];
const res = await fetch(\`/api/search?q=\${encodeURIComponent(query)}\`);
return res.json();
});
// Stores
const $query = restore(queryChanged, ''); // createStore + .on в одну строку
const $results = createStore<SearchResult[]>([]);
const $error = createStore<string | null>(null);
// Дебаунс 300ms перед запросом
const debouncedQuery = debounce({ source: queryChanged, timeout: 300 });
// Логика: при изменении query (с дебаунсом) → запускаем поиск
sample({ clock: debouncedQuery, target: searchFx });
// Обновление сторов
$results.on(searchFx.doneData, (_, results) => results).reset(queryChanged);
$error.on(searchFx.failData, (_, err) => err.message).reset(queryChanged);
// Параллельно: запускаем оба эффекта одновременно
sample({ clock: pageOpened, target: [fetchUserFx, fetchSettingsFx] });
// Последовательно: после успеха первого запускаем второй
sample({
clock: fetchUserFx.doneData,
fn: (user) => user.id,
target: fetchUserPostsFx,
});

Попробуйте примеры в интерактивном редакторе: