Wrapping React Query's useQuery (A Use Case for Wrapping External Libraries)
Tags: react, javascript, webdev, tutorial
Published: 2021-12-21 03:47:14

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:

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

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:

```jsx
// 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();
});
```

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:

```jsx
// 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');
});
```

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

```jsx
// 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');
});
```

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

```jsx
// 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');
```

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:

```jsx
// 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');
```

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