Usage with React Query
The problem of “where to put the keys”
Section titled “The problem of “where to put the keys””Solution — break down by entities
Section titled “Solution — break down by entities”If the project already has a division into entities, and each request corresponds to a single entity, the purest division will be by entity. In this case, we suggest using the following structure:
Directorysrc/
Directoryapp/
- …
Directorypages/
- …
Directoryentities/
Directory{entity}/
- …
Directoryapi/
{entity}.queryQuery-factory where are the keys and functionsget-{entity}Entity getter functioncreate-{entity}Entity creation functionupdate-{entity}Entity update functiondelete-{entity}Entity delete function- …
Directoryfeatures/
- …
Directorywidgets/
- …
Directoryshared/
- …
If there are connections between the entities (for example, the Country entity has a field-list of City entities), then you can use the public API for cross-imports or consider the alternative solution below.
Alternative solution — keep it in shared
Section titled “Alternative solution — keep it in shared”In cases where entity separation is not appropriate, the following structure can be considered:
Directorysrc/
- …
Directoryshared/
Directoryapi/
- …
Directoryqueries Query-factories
- document.ts
- background-jobs.ts
- …
- index.ts
Then in @/shared/api/index.ts:
export { documentQueries } from "./queries/document";The problem of “Where to insert mutations?”
Section titled “The problem of “Where to insert mutations?””It is not recommended to mix mutations with queries. There are two options:
1. Define a custom hook in the api segment near the place of use
Section titled “1. Define a custom hook in the api segment near the place of use”export const useUpdateTitle = () => { const queryClient = useQueryClient();
return useMutation({ mutationFn: ({ id, newTitle }) => apiClient .patch(`/posts/${id}`, { title: newTitle }) .then((data) => console.log(data)),
onSuccess: (newPost) => { queryClient.setQueryData(postsQueries.ids(id), newPost); }, });};2. Define a mutation function somewhere else (Shared or Entities) and use useMutation directly in the component
Section titled “2. Define a mutation function somewhere else (Shared or Entities) and use useMutation directly in the component”const { mutateAsync, isPending } = useMutation({ mutationFn: postApi.createPost,});export const CreatePost = () => { const { classes } = useStyles(); const [title, setTitle] = useState("");
const { mutate, isPending } = useMutation({ mutationFn: postApi.createPost, });
const handleChange = (e: ChangeEvent<HTMLInputElement>) => setTitle(e.target.value); const handleSubmit = (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); mutate({ title, userId: DEFAULT_USER_ID }); };
return ( <form className={classes.create_form} onSubmit={handleSubmit}> <TextField onChange={handleChange} value={title} /> <LoadingButton type="submit" variant="contained" loading={isPending}> Create </LoadingButton> </form> );};Organization of requests
Section titled “Organization of requests”Query factory
Section titled “Query factory”A query factory is an object where the key values are functions that return a list of query keys. Here’s how to use it:
const keyFactory = { all: () => ["entity"], lists: () => [...postQueries.all(), "list"],};1. Creating a Query Factory
Section titled “1. Creating a Query Factory”import { keepPreviousData, queryOptions } from "@tanstack/react-query";import { getPosts } from "./get-posts";import { getDetailPost } from "./get-detail-post";import { PostDetailQuery } from "./query/post.query";
export const postQueries = { all: () => ["posts"],
lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) => queryOptions({ queryKey: [...postQueries.lists(), page, limit], queryFn: () => getPosts(page, limit), placeholderData: keepPreviousData, }),
details: () => [...postQueries.all(), "detail"], detail: (query?: PostDetailQuery) => queryOptions({ queryKey: [...postQueries.details(), query?.id], queryFn: () => getDetailPost({ id: query?.id }), staleTime: 5000, }),};2. Using Query Factory in application code
Section titled “2. Using Query Factory in application code”import { useParams } from "react-router-dom";import { postApi } from "@/entities/post";import { useQuery } from "@tanstack/react-query";
type Params = { postId: string;};
export const PostPage = () => { const { postId } = useParams<Params>(); const id = parseInt(postId || ""); const { data: post, error, isLoading, isError, } = useQuery(postApi.postQueries.detail({ id }));
if (isLoading) { return <div>Loading...</div>; }
if (isError || !post) { return <>{error?.message}</>; }
return ( <div> <p>Post id: {post.id}</p> <div> <h1>{post.title}</h1> <div> <p>{post.body}</p> </div> </div> <div>Owner: {post.userId}</div> </div> );};Benefits of using a Query Factory
Section titled “Benefits of using a Query Factory”- Request structuring: A factory allows you to organize all API requests in one place, making your code more readable and maintainable.
- Convenient access to queries and keys: The factory provides convenient methods for accessing different types of queries and their keys.
- Query Refetching Ability: The factory allows easy refetching without the need to change query keys in different parts of the application.
Pagination
Section titled “Pagination”In this section, we’ll look at an example of the getPosts function, which makes an API request to retrieve post entities using pagination.
1. Creating a function getPosts
Section titled “1. Creating a function getPosts”The getPosts function is located in the get-posts.ts file, which is located in the api segment
import { apiClient } from "@/shared/api/base";
import { PostWithPaginationDto } from "./dto/post-with-pagination.dto";import { PostQuery } from "./query/post.query";import { mapPost } from "./mapper/map-post";import { PostWithPagination } from "../model/post-with-pagination";
const calculatePostPage = (totalCount: number, limit: number) => Math.floor(totalCount / limit);
export const getPosts = async ( page: number, limit: number,): Promise<PostWithPagination> => { const skip = page * limit; const query: PostQuery = { skip, limit }; const result = await apiClient.get<PostWithPaginationDto>("/posts", query);
return { posts: result.posts.map((post) => mapPost(post)), limit: result.limit, skip: result.skip, total: result.total, totalPages: calculatePostPage(result.total, limit), };};2. Query factory for pagination
Section titled “2. Query factory for pagination”The postQueries query factory defines various query options for working with posts,
including requesting a list of posts with a specific page and limit.
import { keepPreviousData, queryOptions } from "@tanstack/react-query";import { getPosts } from "./get-posts";
export const postQueries = { all: () => ["posts"], lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) => queryOptions({ queryKey: [...postQueries.lists(), page, limit], queryFn: () => getPosts(page, limit), placeholderData: keepPreviousData, }),};3. Use in application code
Section titled “3. Use in application code”export const HomePage = () => { const itemsOnScreen = DEFAULT_ITEMS_ON_SCREEN; const [page, setPage] = usePageParam(DEFAULT_PAGE); const { data, isFetching, isLoading } = useQuery( postApi.postQueries.list(page, itemsOnScreen), ); return ( <> <Pagination onChange={(_, page) => setPage(page)} page={page} count={data?.totalPages} variant="outlined" color="primary" /> <Posts posts={data?.posts} /> </> );};QueryProvider for managing queries
Section titled “QueryProvider for managing queries”In this guide, we will look at how to organize a QueryProvider.
1. Creating a QueryProvider
Section titled “1. Creating a QueryProvider”The file query-provider.tsx is located at the path @/app/providers/query-provider.tsx.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";import { ReactQueryDevtools } from "@tanstack/react-query-devtools";import { ReactNode } from "react";
type Props = { children: ReactNode; client: QueryClient;};
export const QueryProvider = ({ client, children }: Props) => { return ( <QueryClientProvider client={client}> {children} <ReactQueryDevtools /> </QueryClientProvider> );};2. Creating a QueryClient
Section titled “2. Creating a QueryClient”QueryClient is an instance used to manage API requests.
The query-client.ts file is located at @/shared/api/query-client.ts.
QueryClient is created with certain settings for query caching.
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000, }, },});Code generation
Section titled “Code generation”There are tools that can generate API code for you, but they are less flexible than the manual approach described above.
If your Swagger file is well-structured,
and you’re using one of these tools, it might make sense to generate all the code in the @/shared/api directory.
Additional advice for organizing RQ
Section titled “Additional advice for organizing RQ”API Client
Section titled “API Client”Using a custom API client class in the shared layer, you can standardize the configuration and work with the API in the project. This allows you to manage logging, headers and data exchange format (such as JSON or XML) from one place. This approach makes it easier to maintain and develop the project because it simplifies changes and updates to interactions with the API.
import { API_URL } from "@/shared/config";
export class ApiClient { private baseUrl: string;
constructor(url: string) { this.baseUrl = url; }
async handleResponse<TResult>(response: Response): Promise<TResult> { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); }
try { return await response.json(); } catch (error) { throw new Error("Error parsing JSON response"); } }
public async get<TResult = unknown>( endpoint: string, queryParams?: Record<string, string | number>, ): Promise<TResult> { const url = new URL(endpoint, this.baseUrl);
if (queryParams) { Object.entries(queryParams).forEach(([key, value]) => { url.searchParams.append(key, value.toString()); }); } const response = await fetch(url.toString(), { method: "GET", headers: { "Content-Type": "application/json", }, });
return this.handleResponse<TResult>(response); }
public async post<TResult = unknown, TData = Record<string, unknown>>( endpoint: string, body: TData, ): Promise<TResult> { const response = await fetch(`${this.baseUrl}${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), });
return this.handleResponse<TResult>(response); }}
export const apiClient = new ApiClient(API_URL);