Skip to content

Instantly share code, notes, and snippets.

@dovranJorayev
Created January 20, 2026 11:38
Show Gist options
  • Select an option

  • Save dovranJorayev/04f18dd883d9efe4c79fce4cebc56674 to your computer and use it in GitHub Desktop.

Select an option

Save dovranJorayev/04f18dd883d9efe4c79fce4cebc56674 to your computer and use it in GitHub Desktop.
fetch based, JSON responded effect factory to create effects that can be used as is and as part of farfetched remote operation
import {
configurationError,
Contract,
httpError,
HttpError,
invalidDataError,
InvalidDataError,
networkError,
NetworkError,
onAbort,
preparationError,
PreparationError,
} from '@farfetched/core';
import { createEffect } from 'effector';
export interface RequestConfig extends RequestInit {
url: string | URL;
}
export type FetchError =
| HttpError
| PreparationError
| NetworkError
| InvalidDataError
| Error;
/**
* Error creators below should be covered
* @see https://ff.effector.dev/api/utils/error_creators.html#error-creators
*
* - [x] abortError (handled by farfetched in operation level)
* - [x] configurationError
* - [x] httpError
* - [x] invalidDataError
* - [x] networkError
* - [x] timeoutError (handled by farfetched in operation level)
* - [x] preparationError
*
* @see https://ff.effector.dev/api/utils/on_abort.html
*
* onAbort is passed to the effect
* @example
* ```ts
* const getTasksFx = createJsonFetchEffect({
* contract: zodContract(GetTasksDataSchema),
* request: (arg) => ({
* url: makeUrl('/api/v1/image/tasks'),
* method: 'GET',
* body: (arg) => ({
* url: arg.url,
* }),
* }),
* })
* ```
*
* Other helpfull links @see https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
*/
export function createJsonFetchEffect<Params = void, Data = unknown>(config: {
request: RequestConfig | ((params: Params) => RequestConfig);
contract: Contract<unknown, Data>;
extra?: {
/**
* @default true
* @description controls does onAbort hook is applied to the effect
* or not. If effect intended to use separately from operation,
* should be set to false.
*/
onAbortHook?: boolean;
};
}) {
const fetchFx = createEffect<Params, Data, FetchError>(async (params) => {
const disposers: Array<() => void> = [];
const defaultedExtra = {
onAbortHook: config.extra?.onAbortHook ?? true,
} satisfies NonNullable<Required<typeof config.extra>>;
try {
let requestInfoWithUrl: RequestConfig;
if (typeof config.request === 'function') {
try {
requestInfoWithUrl = config.request(params);
} catch (error) {
const unsafeError = error as { message?: string } | undefined;
const message =
typeof unsafeError?.message === 'string'
? unsafeError.message
: 'Unknown error';
throw configurationError({
reason: message,
validationErrors: [`Failed to create request due to: ${message}`],
});
}
} else {
requestInfoWithUrl = config.request;
}
const { url, signal, ...requestInit } = requestInfoWithUrl;
const abortCtrl = new AbortController();
if (defaultedExtra.onAbortHook) {
try {
onAbort(() => {
abortCtrl.abort();
});
} catch {
/**
* Error thrown by farfetched/core suppressed by reason of make effect available
* to use outside of operation level is flag is true.
* It should not cause performace degradation issue because it
* custom serializable error instead of Error based one
*/
}
}
if (signal) {
const abortHandler = () => {
if (abortCtrl.signal.aborted) return;
abortCtrl.abort(signal.reason);
};
signal.addEventListener('abort', abortHandler);
disposers.push(() => {
signal.removeEventListener('abort', abortHandler);
});
}
const request = new Request(url, {
...requestInit,
signal: abortCtrl.signal,
});
const response = await fetch(request).catch((cause) => {
throw networkError({
cause,
reason: 'Network error',
});
});
const data = await response.json().catch((error) => {
throw preparationError({
response: 'Bad JSON response',
reason:
typeof error?.message === 'string'
? error.message
: 'Bad JSON response',
});
});
if (!response.ok) {
throw httpError({
status: response.status,
statusText: response.statusText,
response: data,
});
}
if (!config.contract.isData(data)) {
throw invalidDataError({
response: data,
validationErrors: config.contract.getErrorMessages(data),
});
}
return data;
} finally {
disposers.forEach((disposer) => disposer());
disposers.length = 0;
}
});
return fetchFx;
}
export const postFilesToStorageFx = createJsonFetchEffect({
request: ({ file, signal }: PostFilesToStorageArg) => {
const formData = new FormData();
formData.set('file', file);
return {
url: makeUrl('/api/v1/image/upload'),
method: 'POST',
body: formData,
signal,
};
},
contract: zodContract(PostFilesToStorageDataSchema),
extra: {
onAbortHook: false, // if you want to use it as standalone effect along side with mutation use-case
},
});
export const createPostFilesToStorageMutation = createFactory(() =>
createMutation({ effect: postFilesToStorageFx }),
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment