Skip to content
On this page

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:

js
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:

js
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:

  1. Parse the response as JSON and apply Contract to it because we have to be suspicious about the server responses.
  2. Allow Farfetched to cancel the Mutation if application has to.
  3. 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:

js
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:

js
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:

js
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:

js
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:

js
function createUploadFileMutation() {
  return createMutation({
    /* ... */
  });
}

We just moved the Mutation creation into a function. Now we can create as many Mutations as we need:

js
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:

js
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:

json
{
  "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.

sh
pnpm add --save-dev effector-swc-plugin @swc/core
sh
yarn add --dev effector-swc-plugin @swc/core
sh
npm install --dev effector-swc-plugin @swc/core

Now just modify your .swcrc config to enable installed plugin:

json
{
  "$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:

js
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:

js
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.

Released under the MIT License.