Загрузка файлов (FormData)
Загрузка файла — это обычный POST с телом FormData, отдельной поддержки не нужно. Соберите FormData внутри эффекта createRequestFx и передайте в createMutation.
import { createRequestFx, createMutation } from 'effector-refetch';
interface UploadResult {
url: string;
}
const uploadFx = createRequestFx<{ file: Blob; name: string; tags: string[] }, UploadResult>(
({ file, name, tags }, { signal }) => {
const body = new FormData();
body.append('file', file, 'upload.txt');
body.append('name', name);
tags.forEach((tag) => body.append('tags', tag)); // повторяющееся поле = массив
return fetch('/api/upload', { method: 'POST', body, signal }).then((r) => r.json());
},
);
export const uploadMutation = createMutation({ effect: uploadFx, retry: 1 });
uploadMutation.mutate({ file, name: 'avatar', tags: ['a', 'b'] });Не выставляйте Content-Type
Оставьте его — рантайм сам проставит multipart/form-data с boundary. Если выставить вручную, запрос сломается.
Всё остальное — обычная мутация: $pending крутит спиннер, $error — нормализованный RequestError, retry перезаливает, а cancel() / reset() прерывают запрос в полёте через signal (загрузка реально останавливается).
Прогресс загрузки
fetch не умеет сообщать прогресс отдачи, поэтому для прогресс-бара оберните XMLHttpRequest — это всё ещё просто эффект. Кладите прогресс в стор и уважайте signal:
import { createStore, createEvent } from 'effector';
import { createRequestFx, createMutation } from 'effector-refetch';
const progress = createEvent<number>(); // 0..1
export const $uploadProgress = createStore(0).on(progress, (_, p) => p);
const uploadFx = createRequestFx<{ file: File }, { url: string }>(
({ file }, { signal }) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const body = new FormData();
body.append('file', file);
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) progress(e.loaded / e.total);
});
xhr.addEventListener('load', () =>
xhr.status < 400 ? resolve(JSON.parse(xhr.responseText)) : reject(new Error(`HTTP ${xhr.status}`)),
);
xhr.addEventListener('error', () => reject(new Error('Network error')));
// signal принадлежит запросу — cancel()/reset()/TAKE_LATEST прервут загрузку
signal.addEventListener('abort', () => xhr.abort());
xhr.open('POST', '/api/upload');
xhr.send(body);
}),
);
export const uploadMutation = createMutation({ effect: uploadFx });progress — обычное событие, так что $uploadProgress — обычный стор, биндится через useUnit как любое состояние. Сбрасывайте его на uploadMutation.finished.finally, если нужно обнулять после каждой загрузки.
Сочетайте загрузки с invalidate (обновить галерею после успешной загрузки) или optimisticUpdate (показать новый элемент сразу). Рабочий пример: examples/form-data.ts.