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})
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,
});
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>
}
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>
}
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()
...
}
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!
Top comments (1)
Great job !!