DEV Community

Cover image for Wrapping React Query's useQuery (A Use Case for Wrapping External Libraries)
Michael Mangialardi
Michael Mangialardi

Posted on

Wrapping React Query's useQuery (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:

 const {
   data,
   isError,
   isFetched,
   isLoading,
   ...etc,
 } = useQuery('todos', getTodos);
Enter fullscreen mode Exit fullscreen mode

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

As shown in the example above, the useQuery hook takes in a "query key" (the key for the data in the cache) and a function that "queries" data via an API.

There are possibilities to improve the signature of this hook.

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

For example, you could have one instance of useQuery that uses the native fetch API and another that uses a custom fetch wrapper:

// some-component.js

const result = useQuery('cars', () => {
  const resp = await fetch('/api/v1/cars', { method: 'GET' });
  return await resp.json();
});

// another-component.js
import fetchClient from './fetch-client';

const result = useQuery('cars', async () => {
  const resp = await fetchClient('/api/v1/cars');
  return await resp.json();
});
Enter fullscreen mode Exit fullscreen mode

Given this example, there is a code smell since fetchClient is the intended way to make API requests as it encapsulates logic, error handling, preferred settings, etc.

To improve upon this, we can come up with design patterns that help enforce the reusing of the same fetch client.

One option is to export the custom fetch client and all the modules of React Query from a single file, avoiding the importing/using of React Query directly:

// api.js

const defaultOptions = { method: 'GET' };
export async function fetcher(url, options = defaultOptions) {
  const resp = await fetch(url, options);
  return await resp.json();
}

export * from 'react-query';

// some-component.js
import { fetcher, useQuery } from './api.js';

const result = useQuery('cars', async () => {
  return await fetcher('/api/v1/cars');
});
Enter fullscreen mode Exit fullscreen mode

Alternatively, we may expose the fetcher via a hook (similar to React Redux's useDispatch):

// api.js

const defaultOptions = { method: 'GET' };
async function fetcher(url, options = defaultOptions) {
  const resp = await fetch(url, options);
  return await resp.json();
}

export function useFetcher() {
  return fetcher;
}

export * from 'react-query';

// some-component.js
import { useFetcher, useQuery } from './api.js';

const fetcher = useFetcher();
const result = useQuery('cars', async () => {
  return await fetcher('/api/v1/cars');
});
Enter fullscreen mode Exit fullscreen mode

As a third option, we could conceal the fetcher in a wrapper around useQuery:

// api.js
import { useQuery as baseUseQuery } from 'react-query';

const defaultOptions = { method: 'GET' };
async function fetcher(url, options = defaultOptions) {
  const resp = await fetch(url, options);
  return await resp.json();
}

function useQuery(queryKey, query) {
  return useBaseQuery(queryKey, async () => {
    return await fetcher(query);
  });
}

// some-component.js
import { useQuery } from './api.js';

const result = useQuery('cars', '/api/v1/cars');
Enter fullscreen mode Exit fullscreen mode

The second limitation of the plain useQuery can be seen most clearly in our latest wrapper.

Here, we can predict that the "query key" is likely a subpath on the API route.

Because of that, we can derive the "query key" from the query in our abstraction:

// api.js
import { kebabCase } from 'lodash';
import { useQuery as baseUseQuery } from 'react-query';

const defaultOptions = { method: 'GET' };
async function fetcher(url, options = defaultOptions) {
  const resp = await fetch(url, options);
  return await resp.json();
}

function useQuery(query) {
  return useBaseQuery(kebabCase(query), async () => {
    return await fetcher(`/api/v1/${query}`);
  });
}

// some-component.js
import { useQuery } from './api.js';

const result = useQuery('cars');
Enter fullscreen mode Exit fullscreen mode

๐ŸŽ‰ Just like that we've simplified our API lifecycles by wrapping useQuery to better fit our needs.

Regardless of whether this wrapper (as demonstrated) suits your preferences and needs, I hope it helps show the potential value of wrapping modules from shared libraries.

Top comments (1)

Collapse
 
aguirrealvaro profile image
Alvaro Aguirre

What if we want to send the third parameter options to useQuery throught the custom wrapper? It would be good to add it