Wrapping React Query's useMutation (A Use Case for Wrapping External Libraries)
Tags: javascript, react, webdev, tutorial
Published: 2022-01-21 15:05:26

React Query is a library for fetching and mutating server state via React hooks. In addition to the perk of caching, it also neatly returns metadata representing the various lifecycles of a network request for both querying (read operations) and mutating (create, update, delete operations):

```jsx
 const {
   data,
   error,
   isError,
   isFetched,
   isLoading,
   ...etc,
 } = useQuery('todos', getTodos);

 const {
   data,
   error,
   isError,
   isIdle,
   isSuccess,
   mutate,
   ...etc,
 } = useMutation(deleteTodo);
```

This cuts down on the boilerplate when using React local state to track this metadata manually.

As shown in the example above, the `useQuery` and `useMutation` hooks both have an argument for a function that will presumably make the network request (`getTodos` and `deleteTodo` respectively in our example).

I have [previously written about alternatives to the signature of this hook that you can achieve by wrapping it](https://dev.to/michaelmangial1/wrapping-react-querys-usequery-a-use-case-for-wrapping-external-libraries-34ed).

In this post, I'd like to ponder on potential ways to improve the signature of the `useMutation` hook.

First, there is currently no way of enforcing that all mutation functions go through the same API client.

Imagine you wanted to set a pattern in the codebase to make all API requests through a wrapper around the native `fetch` API. That way, some common logic can be encapsulated (like stringifying the request `body`).

Let's say this wrapper is called `fetcher`.

We would want to avoid one mutation function using the native `fetch` API and the other using `fetcher`.

Of course, this could be enforced via code reviews, but what if there was a way to "document" the expected behavior through a design pattern?

A slight improvement to inlining the functions in the file where `useMutation` is called would be to colocate all the operations for an API endpoint in a single file, exporting each function individually.

Then, each "operations" file (the file with all the operations for an API endpoint, including queries and mutations) would have a pattern of importing the `fetcher` module and consuming:

```jsx
/* /api/todos.js */

import fetcher from './fetcher';

export async function getTodos() {
  await fetcher('/api/v1/todos');
}

export async function deleteTodo(id) {
  await fetcher(`/api/v1/todos/${id}`, {
    method: 'DELETE',
  });
}

/* some-component.js */
import { useQuery, useMutation } from 'react-query';

import { getTodos, deleteTodo } from '../api/todos';

function SomeComponent() {
  const todos = useQuery('todos', getTodos);

  const removeTodo = useMutation(deleteTodo);
  
  // not necessary, but wanted to showcase the `.mutate` in action
  function handleRemoveTodo(id) {
    removeTodo.mutate(id);
  }

  if (todos.isLoading) {
    return '...';
  }

  return '...';
}
```

Looks fine, but there's something else to consider.

It is very common to "refresh" (or "requery") after doing a mutation.

In this example, you would want to refresh the todos after deleting one (you could do optimistic updates, but I'm ignoring that for the sake of simplicity). 

To do this, you have to obtain access to `queryClient` via the `useQueryClient` hook, and then "invalidate" the todos query using `queryClient.invalidateQueries` function:

```jsx
const queryClient = useQueryClient();
queryClient.invalidateQueries('todos');
```

The name of `invalidateQueries` captures the technical sense of what's going on.

To "refresh" your todos, you mark the todos as "stale" (effectively saying, "Hey! I may need to update the cached query results via an API request.").

Here's what that would look like in our previous example:

```jsx
/* some-component.js */
import { useQuery, useMutation, useQueryClient } from 'react-query';

import { getTodos, deleteTodo } from '../api/todos';

function SomeComponent() {
  const todos = useQuery('todos', getTodos);

  const removeTodo = useMutation(deleteTodo);

  const queryClient = useQueryClient();
  
  async function handleRemoveTodo(id) {
    await removeTodo.mutateAsync(id);
    queryClient.invalidateQueries('todos');
  }

  if (todos.isLoading) {
    return '...';
  }

  return '...';
}
```

We can potentially improve this by encapsulating `useQueryClient` and the query invalidation into a custom hook (and it provides an opportunity to come up with a preferred name to describe this logic):

```jsx
/* /api/index.js */
export function useRefresh() {
  const queryClient = useQueryClient();
  return (query) => queryClient.invalidateQueries(query);
}

/* some-component.js */
import { useQuery, useMutation } from 'react-query';

import { useRefresh, getTodos, deleteTodo } from '../api';

function SomeComponent() {
  const todos = useQuery('todos', getTodos);

  const removeTodo = useMutation(deleteTodo);

  const refresh = useRefresh();
  
  async function handleRemoveTodo(id) {
    await removeTodo.mutateAsync(id);
    refresh('todos');
  }

  if (todos.isLoading) {
    return '...';
  }

  return '...';
}
```

Lastly, if we wanted to inline the mutation function (`deleteTodo`) while ensuring the same fetch client is used every time, we could expose a hook from the same file as `useRefresh` that returns the fetch client for mutations:

```jsx
/* /api/index.js */
import fetcher from './fetcher';

export function useRequest() {
  // Add any mutation-specific request logic here
  return fetcher;
}

/* some-component.js */
import { useQuery, useMutation } from 'react-query';

import { useRefresh, getTodos, deleteTodo } from '../api';

function SomeComponent() {
  const todos = useQuery('todos', getTodos);

  const request = useRequest();
  const refresh = useRefresh();
  const removeTodo = useMutation(async (id) => {
    await request(`/api/v1/todos/${id}`, {
      method: 'DELETE',
    });

    refresh('todos');
  });
  
  function handleRemoveTodo(id) {
    removeTodo.mutate(id);
  }

  if (todos.isLoading) {
    return '...';
  }

  return '...';
}
```

## Conclusion
Maybe you like these changes, maybe you don't. Either way, I hope this gets the brain juices flowing to consider ways to wrap React Query's `useMutation` to fit the needs of your codebase.