66. Effector: Effects
Effector: Effects — асинхронность без боли
Заголовок раздела «Effector: Effects — асинхронность без боли»Асинхронность — это боль в большинстве state managers. Redux требует thunk/saga. MobX требует flow или action-декораторов. А в Effector есть Effect — первоклассный гражданин, который берёт всю async-логику на себя! 🚀
Представь Effect как «умный кнопочный выключатель»: нажал → он сам отслеживает, идёт ли ток (pending), пришло ли питание (done) или произошло короткое замыкание (fail).
createEffect() — создаём эффект
Заголовок раздела «createEffect() — создаём эффект»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;}
// Effectconst 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();});
// Storesconst $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 storeconst $pageState = combine({ user: $user, error: $error, isLoading: fetchUserFx.pending,});
// ВызовfetchUserFx(1);// fetchUserFx.pending => true// ... (ждём)// fetchUserFx.doneData => { id: 1, name: 'Яша', email: '...' }// fetchUserFx.pending => falseОбработка множественных запросов: inFlight
Заголовок раздела «Обработка множественных запросов: inFlight»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// После завершения всех => 0sample() — оркестрация событий
Заголовок раздела «sample() — оркестрация событий»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 → запустить searchFxsample({ clock: searchClicked, // триггер source: $query, // откуда берём данные target: searchFx, // куда отправляем});
// Теперь при searchClicked() автоматически вызовется searchFx с текущим значением $query!guard() / filter — условное выполнение
Заголовок раздела «guard() / filter — условное выполнение»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,});forward() — перенаправление потоков
Заголовок раздела «forward() — перенаправление потоков»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 и Effectsconst 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();});
// Storesconst $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,});🔗 Полезные ссылки
Заголовок раздела «🔗 Полезные ссылки»Практика
Заголовок раздела «Практика»Попробуйте примеры в интерактивном редакторе: