HTTP и валидация
createRequestFx
Оборачивает любой HTTP-клиент в типизированный, abort-aware эффект с нормализованными ошибками:
import { ofetch } from 'ofetch';
import { createRequestFx, createQuery } from 'effector-refetch';
const getUserFx = createRequestFx<{ id: number }, User>(({ id }, { signal }) =>
ofetch(`/api/users/${id}`, { signal }),
);
const userQuery = createQuery({ effect: getUserFx, cache: true });Хендлер получает AbortSignal; контроллером владеет query и срабатывает им на cancel / reset и при вытеснении TAKE_LATEST — так запрос реально прерывается. Ошибки нормализуются в RequestError (status, data). Возвращает AbortableEffect (экспортируется, если нужно аннотировать); передаётся в createQuery / createMutation и т.д. как обычный Effect.
Error guards
Сужайте payload $error / finished.fail типизированными guard'ами вместо instanceof + кастов:
import { isRequestError, isHttpError, isTimeoutError, isValidationError } from 'effector-refetch';
isHttpError(error); // RequestError со status
isHttpError(error, 404); // ровно 404
isHttpError(error, (s) => s >= 500); // любой 5xx
isTimeoutError(error); // прерван по `timeout`
isValidationError(error); // провал контракта / validate (сужает к .validationErrors)
isRequestError(error); // любая нормализованная транспортная ошибкаimport { sample } from 'effector';
// обновить токен на 401
sample({
clock: api.finished.fail,
filter: ({ error }) => isHttpError(error, 401),
target: authBarrier.lock,
});
// понятное сообщение под каждый тип ошибки
const $message = api.$error.map((e) =>
isHttpError(e, (s) => s >= 500) ? 'Ошибка сервера' : isTimeoutError(e) ? 'Таймаут' : e ? 'Сбой' : null,
);Подробнее — в рецепте обработки ошибок.
Это просто эффект — внутри работает что угодно: multipart FormData-загрузки (examples/form-data.ts), GraphQL (POST { query, variables } — см. рецепт GraphQL), или стриминг (SSE и WebSocket).
createJsonQuery
Декларативный эндпоинт поверх глобального fetch (без зависимости от HTTP-клиента):
import { createJsonQuery, HTTP_METHODS, zodContract } from 'effector-refetch';
export const getProductsQuery = createJsonQuery({
request: { url: 'https://api/products', query: ({ search }) => ({ search, limit: 20 }) },
response: { contract: zodContract(ProductList) },
concurrency: 'TAKE_LATEST',
cache: { staleAfter: 30_000 },
});
export const createUser = createJsonQuery<NewUser, User>({
request: { url: 'https://api/users', method: HTTP_METHODS.POST, body: (u) => u },
});request: { url, method?, query?, body?, headers? }. Каждое поле — функция от параметров (или, для url, статическая строка). Abort-aware, нормализованный RequestError, опциональный контракт и все обычные опции запроса.
Sourced-поля (реактивные, fork-корректные)
Любое поле запроса можно читать из Store — удобно для токена авторизации или base URL, которые лежат в состоянии. Это разводится через attach, поэтому каждый fork/SSR-scope использует своё значение:
const userQuery = createJsonQuery<{ id: number }>({
request: {
// комбинируем стор с параметрами через { source, fn }
url: { source: $apiBase, fn: (base, { id }) => `${base}/users/${id}` },
// или передаём Store напрямую
headers: { source: $token, fn: (token) => ({ authorization: `Bearer ${token}` }) },
},
});Поле — это (params) => T, Store<T> или { source: Store, fn: (value, params) => T }. Сторы резолвятся на момент запроса в рамках scope — без глобального мутабельного клиента.
createJsonMutation
Зеркало createJsonQuery для записей: та же форма request (включая sourced-поля), по умолчанию POST, возвращает Mutation (без cache / refresh / stale).
import { createJsonMutation, invalidate } from 'effector-refetch';
const createUser = createJsonMutation<NewUser, User>({
request: { url: 'https://api/users', body: (u) => u }, // метод по умолчанию POST
});
const deleteUser = createJsonMutation<number>({
request: { url: (id) => `https://api/users/${id}`, method: HTTP_METHODS.DELETE },
});
invalidate({ on: createUser, refetch: usersQuery }); // рефетч списка при успехе
createUser.mutate({ name: 'Ada' });createJsonRequestFx
Переиспользуемый кирпичик за обоими: декларативный request-эффект (та же форма request, sourced-поля, abort-aware, нормализованный RequestError), который можно передать куда угодно, где ждут эффект — createQuery / createMutation / createInfiniteQuery / connectQuery — вместо ручного createRequestFx.
import { createJsonRequestFx, createInfiniteQuery } from 'effector-refetch';
const fetchPageFx = createJsonRequestFx<{ params: { tag: string }; pageParam: number }, Page>({
url: ({ params, pageParam }) => `/api/feed?tag=${params.tag}&cursor=${pageParam}`,
});
const feed = createInfiniteQuery({
effect: fetchPageFx,
initialPageParam: 0,
getNextPageParam: ({ lastPage }) => lastPage.nextCursor,
});Валидация (контракты)
Проверяет ответ по схеме; провал превращается в ретраябельную ValidationError:
import {
createQuery,
zodContract,
standardSchemaContract,
runtypesContract,
ioTsContract,
createContract,
} from 'effector-refetch';
createQuery({ effect: getUserFx, contract: zodContract(UserSchema) }); // zod
createQuery({ effect: getUserFx, contract: standardSchemaContract(UserSchema) }); // valibot / zod 3.24+ / arktype
createQuery({ effect: getUserFx, contract: runtypesContract(User) }); // runtypes
createQuery({ effect: getUserFx, contract: ioTsContract(User) }); // io-ts codec
createQuery({ effect: getUserFx, contract: createContract({ isData: isUser }) }); // вручную / любая либа
createQuery({ effect: getPriceFx, validate: ({ result }) => result >= 0 || ['отрицательная цена'] });Контракты структурные — сами библиотеки схем не импортируются, вы передаёте свою схему/валидатор. При провале $error — это ValidationError с .validationErrors.
contract и validate — два поля, которые композируются: если заданы оба, сначала отрабатывает contract, затем validate, и побеждает первый провал. Это намеренное, финальное решение — они остаются раздельными (проверка по схеме vs. ad-hoc предикат), а не сливаются в одно поле.
@withease/contracts — без адаптера
@withease/contracts отдаёт объекты ровно той формы { isData, getErrorMessages }, что ожидает опция contract, — поэтому его комбинаторы передаются напрямую, без обёртки:
import { obj, str, num, arr } from '@withease/contracts';
createQuery({ effect: getUserFx, contract: obj({ id: num, name: str, tags: arr(str) }) });Любая другая библиотека
Что угодно — в одну строку через createContract (superstruct, typed-contracts, ручной guard):
import { is } from 'superstruct';
createContract({ isData: (raw) => is(raw, UserStruct) });standardSchemaContract уже покрывает любую Standard Schema либу (valibot, arktype, zod ≥3.24).