Wrapping React Query’s useMutation (A Use Case for Wrapping External Libraries)

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 mutati…


This content originally appeared on DEV Community and was authored by Michael Mangialardi

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

 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.

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:

/* /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:

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:

/* 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):

/* /api/index.js */
export function useRefetch() {
  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:

/* /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.


This content originally appeared on DEV Community and was authored by Michael Mangialardi


Print Share Comment Cite Upload Translate Updates
APA

Michael Mangialardi | Sciencx (2022-01-21T15:05:26+00:00) Wrapping React Query’s useMutation (A Use Case for Wrapping External Libraries). Retrieved from https://www.scien.cx/2022/01/21/wrapping-react-querys-usemutation-a-use-case-for-wrapping-external-libraries/

MLA
" » Wrapping React Query’s useMutation (A Use Case for Wrapping External Libraries)." Michael Mangialardi | Sciencx - Friday January 21, 2022, https://www.scien.cx/2022/01/21/wrapping-react-querys-usemutation-a-use-case-for-wrapping-external-libraries/
HARVARD
Michael Mangialardi | Sciencx Friday January 21, 2022 » Wrapping React Query’s useMutation (A Use Case for Wrapping External Libraries)., viewed ,<https://www.scien.cx/2022/01/21/wrapping-react-querys-usemutation-a-use-case-for-wrapping-external-libraries/>
VANCOUVER
Michael Mangialardi | Sciencx - » Wrapping React Query’s useMutation (A Use Case for Wrapping External Libraries). [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/01/21/wrapping-react-querys-usemutation-a-use-case-for-wrapping-external-libraries/
CHICAGO
" » Wrapping React Query’s useMutation (A Use Case for Wrapping External Libraries)." Michael Mangialardi | Sciencx - Accessed . https://www.scien.cx/2022/01/21/wrapping-react-querys-usemutation-a-use-case-for-wrapping-external-libraries/
IEEE
" » Wrapping React Query’s useMutation (A Use Case for Wrapping External Libraries)." Michael Mangialardi | Sciencx [Online]. Available: https://www.scien.cx/2022/01/21/wrapping-react-querys-usemutation-a-use-case-for-wrapping-external-libraries/. [Accessed: ]
rf:citation
» Wrapping React Query’s useMutation (A Use Case for Wrapping External Libraries) | Michael Mangialardi | Sciencx | https://www.scien.cx/2022/01/21/wrapping-react-querys-usemutation-a-use-case-for-wrapping-external-libraries/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.