FormData
in Mutation
Sometimes you need to send a file or a blob to the server. In this case, you can use the FormData
object.
Plain solution
It can be done in with a simple JS-function:
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
return fetch('/upload', {
method: 'POST',
body: formData,
});
}
Now, let's connect it to the Mutation:
import { createMutation } from '@farfetched/core';
const uploadFileMutation = createMutation({ handler: uploadFile });
That is it! Now you can use uploadFileMutation
across your application as any other Mutation.
Enhancements
However, it would be nice to have some additional features:
- Parse the response as JSON and apply Contract to it because we have to be suspicious about the server responses.
- Allow Farfetched to cancel the Mutation if application has to.
- Provide a way to create as many Mutations to upload different files as we need.
Let us implement these features one by one.
Parse the response as JSON
Actually, it is very easy to do. We just need to call .json
method of the response and handle the possible errors:
import { createMutation, preparationError } from '@farfetched/core';
const uploadFileMutation = createMutation({
handler: uploadFile,
effect: createEffect(async (file) => {
const response = await uploadFile(file);
try {
const parsedJson = await response.json();
return parsedJson;
} catch (e) {
throw preparationError({ reason: 'Response is not JSON' });
}
}),
});
Note that we catch the error and throw a new one. It is important because we want to have a unified error handling across the application and distinguish the errors by error guards.
Apply the Contract
The next step is to apply the Contract to the parsed JSON. Luckily, createMutation
has a special option for that:
import { createMutation, preparationError } from '@farfetched/core';
const uploadFileMutation = createMutation({
effect: createEffect(async (file) => {
const response = await uploadFile(file);
try {
const parsedJson = await response.json();
return parsedJson;
} catch (e) {
throw preparationError({ reason: 'Response is not JSON' });
}
}),
contract: UploadFileResponseContract,
});
To better understand Contracts, please read tutorial articles about it.
Allow Farfetched to cancel the Mutation
To cancel the Mutation, we need to use the AbortController
API. It is a standard API, so you can use it with any library.
Just create an instance of the AbortController
and pass its signal
to the uploadFile
function:
import { createMutation, preparationError, onAbort } from '@farfetched/core';
const uploadFileMutation = createMutation({
effect: createEffect(async (file) => {
const abortController = new AbortController();
onAbort(() => abortController.abort());
const response = await uploadFile(file, {
signal: abortController.signal,
});
try {
const parsedJson = await response.json();
return parsedJson;
} catch (e) {
throw preparationError({ reason: 'Response is not JSON' });
}
}),
contract: UploadFileResponseContract,
});
Additionally, we need to pass the signal
to the fetch
function:
async function uploadFile(file, { signal }) {
const formData = new FormData();
formData.append('file', file);
return fetch('/upload', {
method: 'POST',
body: formData,
signal,
});
}
That is it! Now we can cancel the uploadFileMutation
as any other Mutation in Farfetched.
Turn it into a factory
Now we have single Mutation to upload a file. However, it would be nice to have a factory to create as many Mutations as we need. Let us turn uploadFileMutation
into a factory:
function createUploadFileMutation() {
return createMutation({
/* ... */
});
}
We just moved the Mutation creation into a function. Now we can create as many Mutations as we need:
const uploadAvatarMutation = createUploadFileMutation();
const uploadPhotoMutation = createUploadFileMutation();
/* ... */
SSR, cache
and DevTools support
Deep dive
If you want to learn more about the reasons behind this requirement, please read this article.
If you use Farfetched in SSR, want to use DevTools or cache
, you need to provide a unique name for each Mutation. It can be done by passing the name
option to the createMutation
factory:
function createUploadFileMutation({ name }) {
return createMutation({
name,
/* ... */
});
}
const uploadAvatarMutation = createUploadFileMutation({
name: 'uploadAvatar',
});
const uploadPhotoMutation = createUploadFileMutation({
name: 'uploadPhoto',
});
Code transformations
However, it is not very convenient to pass the name
option every time and control the uniqueness of the names manually. We can do better with automated code transformation.
Babel plugin
If your project already uses Babel, you do not have to install any additional packages, just modify your Babel config with the following plugin:
{
"plugins": ["effector/babel-plugin"]
}
INFO
Read more about effector/babel-plugin
configuration in the Effector's documentation.
SWC plugin
WARNING
Note that plugins for SWC are experimental and may not work as expected. We recommend to stick with Babel for now.
SWC is a blazing fast alternative to Babel. If you are using it, you can install effector-swc-plugin
to get the same DX as with Babel.
pnpm add --save-dev effector-swc-plugin @swc/core
yarn add --dev effector-swc-plugin @swc/core
npm install --dev effector-swc-plugin @swc/core
Now just modify your .swcrc
config to enable installed plugin:
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"experimental": {
"plugins": ["effector-swc-plugin"]
}
}
}
INFO
Read more about effector-swc-plugin
configuration in the plugin documentation.
Vite
If you are using Vite, please read the recipe about it.
Custom factories
Note that code transformations does not support custom factories out of the box. So, you have to explicitly mark you factory as a factory. We recommend using @withease/factories
package for that:
import { createFactory, invoke } from '@withease/factories';
const createUploadFileMutation = createFactory(() => {
return createMutation({
/* ... */
});
});
const uploadAvatarMutation = createUploadFileMutation();
const uploadAvatarMutation = invoke(createUploadFileMutation);
const uploadPhotoMutation = createUploadFileMutation();
const uploadPhotoMutation = invoke(createUploadFileMutation);
FAQ
Q: Why Farfetched does not provide a factory for FormData
?
A: APIs that accept FormData
are very different. Some of them accept only FormData
, some of them accept FormData
and other parameters, some of them accept FormData
and return a response as a plain text, some of them accept FormData
and return a response as JSON, etc.
So, it is quite hard to provide a factory that will cover all possible cases. Since this is a quite rare use case, we decided to not provide a factory for it and let you create your own factory with this recipe.
Q: Why do I need to handle AbortController
manually?
A: All factories in Farfetched are divided into two categories: specific factories and basic factories.
Specific factories like createJsonMutation
provide less flexibility but more convenience. For example, createJsonMutation
handles AbortController
for you.
Basic factories like createMutation
provide more flexibility but less convenience. Since they allow to use any HTTP-transports, they do not handle AbortController
for you because it is impossible to do it in a generic way.
Read more about it in the article about Data flow in Remote Operations.
Conclusion
Congratulations! Now you know how to create a Mutation to upload a file with Farfetched.
The basic usage of FormData
is quite simple:
import { createMutation } from '@farfetched/core';
const uploadFileMutation = createMutation({ handler: uploadFile });
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
return fetch('/upload', {
method: 'POST',
body: formData,
});
}
But it is a lot of room for improvements which is covered in enhancements section.