DEV Community

Cover image for How to organize your API layer in React using React Query
Veljko Sekulic
Veljko Sekulic

Posted on

How to organize your API layer in React using React Query

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:

1. You have your service file:

// 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})
Enter fullscreen mode Exit fullscreen mode

2. Then you create queries/mutation hooks:

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,
  });
Enter fullscreen mode Exit fullscreen mode

3. And finally use it in your component:

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>
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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>
}
Enter fullscreen mode Exit fullscreen mode

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 creatively called react-query-factory for everyone to use.

If you want to contribute or check out the code here’s the GitHub repository.

🥷🏼 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:

// 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()
    ...
}
Enter fullscreen mode Exit fullscreen mode

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/

Top comments (1)

Collapse
 
g1dra profile image
Darko Vucetic

Great job !!