Запросы
import { createQuery } from 'effector-refetch';
const query = createQuery({
effect, // Effect<Params, Result, Error> (или `handler`)
initialData,
enabled, // Store<boolean>
mapData,
mapError,
contract,
validate, // см. «HTTP и валидация»
retry, // number | { times, delay?, filter?, suppressIntermediateErrors? }
cache, // true | { adapter?, staleAfter?, key?, purge?, swr?, dedupe? }
concurrency, // 'TAKE_LATEST' (по умолчанию) | 'TAKE_FIRST' | 'TAKE_EVERY'
name, // метка для devtools
});Опции
effect— вашEffect<Params, Result, Error>.handler: async params => …— сахар.concurrency— поведение при пересекающихся запусках:TAKE_LATEST(по умолчанию) — новый запуск вытесняет и прерывает предыдущий.TAKE_FIRST— игнорировать новые запуски, пока один в полёте.TAKE_EVERY— применяются все (в$dataпобеждает последний результат).QUEUE— прогоны идут строго по очереди (внутри полосы); ошибки цепочку не рвут.- Объектная форма
{ strategy?, key?: (params) => string }добавляет полосы (lanes): запуски с одинаковым ключом конкурируют между собой, разные полосы независимы (обновление одной строки не отменяет соседние).cancel/resetзатрагивают все полосы;$dataостаётся один — полосы разделяют отмену, а не данные.
retry—numberили{ times, delay?, filter?, suppressIntermediateErrors? }. Каждый ретрай — реальный вызов эффекта. Хелперы:linearDelay,exponentialDelay.cache—trueили конфиг (см. кэширование).enabled— гейтStore<boolean>; покаfalse,start/refreshпропускаются.refetchInterval— поллинг каждые N мс (numberилиStore<number>, 0 = выкл). См. Авто-рефетч и поллинг.timeout— дедлайн одной попытки в мс (numberилиStore<number>, 0 = выкл): если прогон превысил его, запрос в полёте прерывается и прогон падает (ретраится, композируется сretry). Не путать сrefetchInterval(как часто поллить) —timeoutэто сколько может длиться одна попытка.debounce— подождать N мс перед запуском (numberилиStore<number>, 0 = выкл); более новый запуск той же полосы во время ожидания вытесняет этот до похода в сеть — честный debounce для поиска по мере ввода под TAKE_LATEST.fallback— превратить финальную ошибку (после ретраев) в данные: значение или({ error, params }) => value;$statusстановитсяdone, срабатываетfinished.done, кэш не пишется; aborts/skips не затрагиваются.structuralSharing— сохранять ссылочную идентичность неизменённых частей результата (меньше ре-рендеров).placeholderData— значение или(prev) => …, показываемое, пока нет реальных данных;$isPlaceholderDataравноtrueдо первого реального результата. В отличие отinitialData, не считается закэшированным.initialData— стартовое значение$data; с ним стор типизируется без null (Store<Data>вместоStore<Data | null>, совместимо с farfetched) — ниже по коду не нужны?.и гварды.mapData/mapError— нормализация результата / ошибки перед сторами.source/mapParams— маппинг публичных params (+ значений сторов изsource, читаются fork-корректно) в параметры эффекта перед каждым прогоном (см. Маппинг параметров).tags— теги инвалидации: совпавшийinvalidateTag(...)чистит кэш-неймспейс запроса и перезапрашивает его с последними params.
$pending истинно для любого прогона. Отличить первую загрузку от фонового рефетча: $isInitialLoading — прогон без реальных данных (плейсхолдер не считается; initialData — считается) — показывайте скелетон; $isRefetching — прогон поверх имеющихся данных (refetch / поллинг / SWR-ревалидация) — данные видимы, спиннер в углу. Запускаемое демо: examples/loading-flags.ts.
Статусные флаги (совместимы с farfetched, производные от $status, у запросов и мутаций): $succeeded ('done'), $failed ('fail'), $finished (завершился любым исходом).
query.prefetch(params) прогревает кэш под params без изменения $data/$status (no-op без кэша, пропускает свежие записи) — например, префетч следующей страницы по hover.
keepPreviousData по умолчанию
$data не очищается на новый start — он держит предыдущий результат, пока не придёт новый. То есть при смене параметров старые данные остаются видимыми, пока идёт новый запрос (TanStack-овский keepPreviousData), из коробки. Для явной очистки — reset().
Разделите это между многими запросами через фабрику.
Бросающие колбэки не роняют граф
Пользовательские колбэки исполняются в чистой фазе графа effector, поэтому throw конвертируется, а не убивает распространение: mapParams / mapData роняют ран (finished.fail), mapError откатывается к исходной ошибке, fallback деградирует до обычной ошибки, validate становится ретраябельной ошибкой валидации; бросающий lane key деградирует к одной полосе, а бросающие предикаты connectQuery / invalidate считаются false.
Всё состояние одним объектом: $state
$state — дискриминированный union: матчинг по status сужает остальные поля, и после проверки статуса больше не нужно data?.:
const state = useUnit(query.$state); // или scope.getState(query.$state)
if (state.status === 'done') {
state.data; // Data — non-null, без каста
state.error; // null — статически
}
if (state.status === 'fail') {
state.error; // Error — non-null
state.data; // Data | null — stale-данные могут остаться
}Флаги (pending, stale, isInitialLoading, isRefetching, isPlaceholderData, enabled, params) едут в каждом варианте. Выводится из гранулярных сторов — подписывайтесь на ту гранулярность, которая нужна.
Императивный запуск: startAsync
query.startAsync — настоящий Effect<Params, Data>: запустить и дождаться исхода:
const user = await userQuery.startAsync(1); // резолвится маппированными данными
// реджектится ошибкой рана или Error с AbortReason при отбрасывании- React/Vue/Solid:
useUnit(query.startAsync)возвращает scope-bound функцию с промисом — идеально для submit-хэндлеров. - Тесты/SSR:
(await allSettled(query.startAsync, { scope, params })).value— данные. - У мутаций есть алиас
mutateAsync:await createTodo.mutateAsync(text). - Вызовы сопоставляются с завершениями по params (глубокое равенство, старейший первым): два scope с ОДИНАКОВЫМИ params в один момент могут обменяться результатами — там, где это важно, используйте
allSettled+scope.getState.
События жизненного цикла
query.finished.done; // { params, result } — запуск успешен
query.finished.fail; // { params, error } — запуск упал
query.finished.finally; // { params, status: 'done' | 'fail' }
query.aborted; // { params, reason } — cancel / reset / вытеснение TAKE_LATEST / skipДля совместимости с farfetched finished также отдаёт:
query.finished.success; // алиас finished.done (то же событие)
query.finished.failure; // алиас finished.fail (то же событие)
query.finished.skip; // { params } — гейт `enabled` заблокировал запускfinished.skip срабатывает только на skip по гейту enabled (запрос не выполнялся). Более широкое событие aborted по-прежнему срабатывает на любой отброшенный запуск — skip, cancel, reset и вытеснение TAKE_LATEST, — оставаясь надмножеством skip. (В отличие от farfetched, finished.finally срабатывает только на done/fail, не на skip — отслеживайте skip через finished.skip / aborted.)
Та же причина едет и на AbortSignal рана: хэндлеры (и ошибки, которые кидает их fetch) видят в signal.reason AbortError с причиной в сообщении — 'cancelled', 'superseded' или 'timeout' для дедлайна.
aborted несёт типизированный reason — почему запуск был отброшен: 'cancelled' (явный cancel/reset), 'superseded' (его вытеснил более новый запуск той же полосы), 'take-first-busy' (TAKE_FIRST отбросил при занятой полосе), 'disabled' (гейт enabled был выключен).
Операторы
concurrency / retry / cache — это ещё и отдельные композируемые операторы; inline-опции — сахар над ними. Применяйте напрямую, в том числе после создания:
import { createQuery, concurrency, retry, cache } from 'effector-refetch';
const search = createQuery({ effect: searchFx });
concurrency(search, { strategy: 'TAKE_LATEST' });
retry(search, { times: 3, delay: exponentialDelay(200) });
cache(search, { staleAfter: 30_000, purge: loggedOut });Кэширование
cache: { adapter?, staleAfter?, key?, purge?, swr?, dedupe? }
swr: true— отдать устаревшую запись сразу, ревалидировать в фоне ($staleпереходитtrue→false).dedupe: true— склеить одинаковые in-flight запросы (по ключу) в один прогон эффекта.- Адаптеры:
inMemoryCache({ maxAge?, maxEntries?, onHit?, onMiss?, onExpired?, onEvicted? })(LRU GC + события),localStorageCache({ version?, maxAge? })/sessionStorageCache(...)(поднимитеversion, чтобы инвалидировать старые данные),voidCache. $queryCache— scope-оверрайд адаптера:fork({ values: [[$queryCache, inMemoryCache()]] })даёт каждому query этого scope изолированный кэш (мультитенантный SSR). См. SSR-рецепт.
Маппинг параметров (source / mapParams)
Идиома attach({ source, mapParams }) как инлайн-опция — «запеките» статические параметры или общее состояние приложения (id пользователя, токен) в каждый прогон, чтобы вызывающий код передавал только то, что меняется. Голый attach тоже работает (включая настоящую отмену); инлайн-опция вдобавок считает ключ кэша от смапленных params и экономит отдельное объявление эффекта:
const $userId = createStore('user-123');
const postsQuery = createQuery({
effect: getPostsFx, // Effect<{ search: string; userId: string; limit: number }, Post[]>
source: { userId: $userId }, // Store или объект Store'ов, читаются fork-корректно
mapParams: (search: string, { userId }) => ({ search, userId, limit: 20 }),
cache: true,
});
postsQuery.start('effector'); // эффект получит { search, userId, limit }- Публичная поверхность query (
start/$params/finished.*/ ctx уmapData) оперирует публичными params ('effector'); эффект видит смапленные. - Ключ кэша (и
cache.key) считается от смапленных params — сменаsourceдаёт другой ключ, поэтому другому пользователю никогда не отдастся запись предыдущего. refetch/ поллинг /keepFreshперечитываютsourceна момент прогона;retryповторяет прогон с маппингом, зафиксированным на старте (ретрай — это тот же запрос).mapParamsдолжен быть чистым — он выполняется внутриfnу sample.
Реактивный (sourced) конфиг
Inline concurrency, retry.times, cache.staleAfter (и enabled) принимают Store вместо константы — читается реактивно и fork-корректно (каждый scope видит своё значение):
const $retries = createStore(0);
createQuery({ effect: fx, retry: { times: $retries, delay: exponentialDelay(200) } });connectQuery
connectQuery({ source, fn, target, filter? }); // один источник
connectQuery({ source: { a, b }, fn, target, filter? }); // несколько (ждёт done всех)fn получает { result, params } по каждому источнику и возвращает { params } для цели.