How to organize your API layer in React using React Query
Tags: react, cleancode, api, reactquery
Published: 2024-02-19 17:07:11

You are all probably using React Query (Tanstack Query) to help you with the data fetching in your React project. If you are not, what are you doing?

React Query is a server state management library for React. It expects that you provide a Promise returning function and it will handle the rest. You will get things like data caching out of the box plus a bunch of utility states like `isLoading, error, isError` etc which will make your life as a developer easier. Oh, did I say that you also get data caching out of the box?

Enough with React Query. Let's move on to see how we've usually dealt with organizing our API layer and how we can improve on it.


## The Old Way of doing things:
<br>

#### 1. You have your service file:

```ts
// api/posts.service.ts
export const getPosts = () => axios.get<Post[]>("http://localhost:5000/posts")
export const createPost = ({title, description}: CreatePostParams) => axios.post<Post>("http://localhost:5000/posts", {title, description})
```

#### 2. Then you create queries/mutation hooks:

```tsx
import {
  UseQueryOptions,
  QueryKey,
  UseMutationOptions,
  MutationFunction,
  useQuery,
} from '@tanstack/react-query';

const useGetPosts = (
  options: UseQueryOptions<Post[], unknown, Post[], QueryKey>,
) => useQuery({ queryKey: ['posts'], queryFn: getPosts, options });

const useCreatePost = (
  options: UseMutationOptions<Post, unknown, CreatePostParams, unknown>,
) =>
  useMutation<Post, unknown, CreatePostParams, unknown>({
    mutationFn: serviceFn as MutationFunction<Post, CreatePostParams>,
    ...options,
  });
```

#### 3. And finally use it in your component:

```tsx
const PostsManager = () => {
	const { data, isLoading } = useGetPosts() // data is fully typesafe
	const { mutate } = useCreatePost() // mutate function is fully typesafe

	const clickHandler = () => {
		mutate({title: "Post title", description: "Post description"})
	}
	return <div>...</div>
}
```

Congratulations! If you are doing this you are on the right path.

But you can notice how things get tedious as our service layer expands. 

There are a couple of problems with this approach:

- **We have to keep track of all of these types (params and response type)**
- **We have to manually create each new query or mutation**
- **Query keys are magic strings with no centralization**

---
## The Centralized way:

Wouldn’t it be an amazing developer experience if we could do something like this:

```tsx
// service layer
const postsService = { getPosts, createPost, getPostById }

// queries layer
const postsQueries = createQueriesFromService(postsService, 'posts')

const PostsManager = () => {
	const { data, isLoading } = postsQueries.getPosts.useQuery() // fully typesafe data and options etc.
	const { mutate } = postQueries.createPost.useMutation() 

	const { data, isLoading } = postsQueries.getPostById.useQuery({id: "123"}) // fully typesafe params etc.
	
	const queryKey = postsQueries.getPostById.queryKey({id: "123"}) // automated query key handling + fully typesafe

	const clickHandler = () => {
		mutate({title: "Post title", description: "Post description"}) // fully typesafe mutate function
	}
	return <div>...</div>
}
```

The service layer is neatly organized and is the single source of truth for our queries layer. There is no manual typing, no manual hook creation.

All of the types are magically inferred. You just need to pass the service object to `createQueriesFromService` function and that’s it.

> "But where do you find this magical `createQueriesFromService` function?"
>
> -- *You probably*

I extracted the code into a separate open source *[npm library](https://www.npmjs.com/package/react-query-factory?activeTab=readme)* creatively called *`react-query-factory`* for everyone to use. 

If you want to contribute or check out the code here’s the [*GitHub repository*](https://github.com/veljkoza/react-query-factory).

> 🥷🏼 You’ll notice I shamelessly stole some concepts from tRPC, but you know what they say about great artists.

For bonus Developer Experience you can have one object which encapsulates all of the queries. For example:

```tsx
// service layer
const postsService = { getPosts, createPost, getPostById };
const productService = { getProducts, addProductToCart, getProductById };

// queries layer
const postsQueries = createQueriesFromService(postsService, "posts");
const productsQueries = 
createQueriesFromService(productService, "products");

// centralized queries
const queries = { posts: postsQueries, products: productsQueries };

const Foo = () => {
	const { data } = queries.products.getProducts.useQuery()
	...
}
```

This way your API layer is centralized in `queries` object, so next developer doesn’t have to second guess names like `productQueries`, since they can rely on autocomplete to offer them suggestions by typing `queries.` and pressing `cmd + space` or `ctrl + space`. 

That’s all folks, thanks for reading!

Linkedin: https://www.linkedin.com/in/sekulic-veljko/