- );
+export const PostFeed = () => {
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
+ useInfiniteQuery(postApi.POST_QUERIES.infinite(10));
+
+ const posts = data?.pages.flatMap((page) => page.posts) ?? [];
+
+ return (
+ <>
+
+ { hasNextPage && (
+
+ ) }
+ >
+ );
};
```
-### 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.
+## Suspense mode \{#suspense-mode\}
-## Pagination
+`useSuspenseQuery` allows you to use React Suspense for handling loading states, removing the need to check `isLoading` manually.
-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. The query factory remains the same \{#suspense-query-factory\}
-### 1. Creating a function `getPosts`
-The getPosts function is located in the `get-posts.ts` file, which is located in the `api` segment
+`queryOptions` and `useSuspenseQuery` are compatible — no changes to the factory are needed.
-```tsx title="@/pages/post-feed/api/get-posts.ts"
-import { apiClient } from "@/shared/api/base";
+### 2. Usage in a component \{#suspense-usage\}
-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";
+```tsx title="src/pages/post/ui/post.tsx"
+import { useSuspenseQuery } from '@tanstack/react-query';
+import { postApi } from '@/shared/api/post';
-const calculatePostPage = (totalCount: number, limit: number) =>
- Math.floor(totalCount / limit);
+interface PostProps {
+ id: number;
+}
-export const getPosts = async (
- page: number,
- limit: number,
-): Promise => {
- const skip = page * limit;
- const query: PostQuery = { skip, limit };
- const result = await apiClient.get("/posts", query);
+// isLoading is no longer needed — the component only renders when data is ready
+export const Post = ({ id }: PostProps) => {
+ const { data: post } = useSuspenseQuery(postApi.POST_QUERIES.detail({ id }));
- return {
- posts: result.posts.map((post) => mapPost(post)),
- limit: result.limit,
- skip: result.skip,
- total: result.total,
- totalPages: calculatePostPage(result.total, limit),
- };
+ return (
+
+
{ post.title }
+
{ post.body }
+
+ );
};
```
-### 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.
-
-```tsx
-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. Wrapper in the `app` layer \{#suspense-app-wrapper\}
+
+```tsx title="src/app/providers/suspense-provider.tsx"
+import { Suspense, type ReactNode } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+
+interface SuspenseProviderProps {
+ children: ReactNode;
+}
+
+export const SuspenseProvider = ({ children }: SuspenseProviderProps) => (
+ Something went wrong }>
+ Loading... }>
+ { children }
+
+
+);
```
+## `useMutationState` \{#use-mutation-state\}
+
+`useMutationState` allows you to read the state of mutations from any component without passing props — useful for global loading indicators or displaying the status of an operation.
+
+### 1. Storing mutation keys \{#storing-mutation-keys\}
-### 3. Use in application code
-
-```tsx title="@/pages/home/ui/index.tsx"
-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 (
- <>
- setPage(page)}
- page={page}
- count={data?.totalPages}
- variant="outlined"
- color="primary"
- />
-
- >
- );
+By analogy with the query factory, mutation keys should be stored in one place alongside the factory:
+
+```ts title="src/shared/api/post/post.queries.ts"
+export const POST_MUTATIONS = {
+ updateTitle: () => ['post', 'update-title'],
+ create: () => ['post', 'create'],
};
```
-
-## `QueryProvider` for managing queries
-In this guide, we will look at how to organize a `QueryProvider`.
+### 2. Naming mutations via `mutationKey` \{#naming-mutations\}
-### 1. Creating a `QueryProvider`
-The file `query-provider.tsx` is located at the path `@/app/providers/query-provider.tsx`.
+```tsx title="src/features/update-post/api/use-update-post-title.ts"
+import { POST_MUTATIONS } from '@/shared/api/post';
-```tsx title="@/app/providers/query-provider.tsx"
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
-import { ReactNode } from "react";
+interface UpdatePostTitle {
+ id: number;
+ newTitle: string;
+}
-type Props = {
- children: ReactNode;
- client: QueryClient;
-};
+export const useUpdatePostTitle = () =>
+ useMutation({
+ mutationKey: POST_MUTATIONS.updateTitle(),
+ mutationFn: ({ id, newTitle }: UpdatePostTitle) =>
+ apiClient.patch(`/posts/${id}`, { title: newTitle }),
+ });
+```
+
+### 3. Reading state in another component \{#reading-mutation-state\}
-export const QueryProvider = ({ client, children }: Props) => {
- return (
-
- {children}
-
-
- );
+```tsx title="src/widgets/save-indicator/ui/save-indicator.tsx"
+import { useMutationState } from '@tanstack/react-query';
+import { POST_MUTATIONS } from '@/shared/api/post';
+
+export const SaveIndicator = () => {
+ const isPending = useMutationState({
+ filters: { mutationKey: POST_MUTATIONS.updateTitle(), status: 'pending' },
+ select: mutation => mutation.state.status,
+ }).length > 0;
+
+ return isPending && (
+ Saving...
+ );
};
```
-### 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.
+## Organising `QueryProvider` \{#query-provider\}
-```tsx title="@/shared/api/query-client.ts"
-import { QueryClient } from "@tanstack/react-query";
+```tsx title="src/app/providers/query-provider.tsx"
+import { type ReactNode } from 'react';
+import { QueryClient, QueryClientProvider, MutationCache, QueryCache } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import { toast } from 'sonner';
-export const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- staleTime: 5 * 60 * 1000,
- gcTime: 5 * 60 * 1000,
+interface QueryProviderProps {
+ children: ReactNode;
+ client: QueryClient;
+}
+
+const queryClient = new QueryClient({
+ queryCache: new QueryCache({
+ onError: error => {
+ toast.error(error.message);
+ },
+ }),
+ mutationCache: new MutationCache({
+ onError: error => {
+ toast.error(error.message);
+ },
+ }),
+ defaultOptions: {
+ queries: {
+ staleTime: 5 * 60 * 1000,
+ gcTime: 5 * 60 * 1000,
+ },
},
- },
});
-```
-## Code generation
+export const QueryProvider = ({ client, children }: QueryProviderProps) => {
+ return (
+
+ { children }
+
+
+ );
+};
+```
-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.
+## Code generation \{#code-generation\}
+There are tools for automatic code generation that are less flexible than the manual approach described above. If your Swagger file is well-structured and you are using one of these tools, it may make sense to generate all the code in the `@/shared/api` directory.
-## Additional advice for organizing RQ
-### API Client
+## Additional advice on API interaction \{#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.
+By using a custom API client class in the `shared` layer, you can standardise the configuration and work with the API across the project. This allows you to manage logging, headers, and the data exchange format (such as JSON or XML) from one place. This approach simplifies maintenance and development by making changes and updates to API interactions easier.
-```tsx title="@/shared/api/api-client.ts"
+```tsx title="src/shared/api/api-client.ts"
import { API_URL } from "@/shared/config";
export class ApiClient {
- private baseUrl: string;
+ #baseUrl: string;
- constructor(url: string) {
- this.baseUrl = url;
- }
-
- async handleResponse(response: Response): Promise {
- if (!response.ok) {
- throw new Error(`HTTP error! Status: ${response.status}`);
+ constructor(url: string) {
+ this.#baseUrl = url;
}
- try {
- return await response.json();
- } catch (error) {
- throw new Error("Error parsing JSON response");
+ async handleResponse(response: Response): Promise {
+ 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(
- endpoint: string,
- queryParams?: Record,
- ): Promise {
- const url = new URL(endpoint, this.baseUrl);
-
- if (queryParams) {
- Object.entries(queryParams).forEach(([key, value]) => {
- url.searchParams.append(key, value.toString());
- });
+
+ public async get(endpoint: string, queryParams?: Record): Promise {
+ 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(response);
}
- const response = await fetch(url.toString(), {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- });
- return this.handleResponse(response);
- }
-
- public async post>(
- endpoint: string,
- body: TData,
- ): Promise {
- const response = await fetch(`${this.baseUrl}${endpoint}`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(body),
- });
+ public async post>(endpoint: string, body: TData): Promise {
+ const response = await fetch(`${ this.#baseUrl }${ endpoint }`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(body),
+ });
- return this.handleResponse(response);
- }
+ return this.handleResponse(response);
+ }
}
export const apiClient = new ApiClient(API_URL);
diff --git a/src/content/docs/ru/docs/guides/tech/with-react-query.mdx b/src/content/docs/ru/docs/guides/tech/with-react-query.mdx
index 1f274061ef..f52ee47d5d 100644
--- a/src/content/docs/ru/docs/guides/tech/with-react-query.mdx
+++ b/src/content/docs/ru/docs/guides/tech/with-react-query.mdx
@@ -1,420 +1,542 @@
---
-title: Использование с React Query
+title: Использование с TanStack Query
sidebar:
- order: 10
+ order: 2
---
-import { FileTree } from '@astrojs/starlight/components';
-import { Aside } from '@astrojs/starlight/components';
+import { Tabs, TabItem, FileTree } from '@astrojs/starlight/components';
+
+## Где располагать ключи \{#where-to-store-keys\}
+
+
+
+
+
+ - src/
+ - app/
+ - pages/
+ - widgets/
+ - features/
+ - entities/
+ - shared/
+ - api/
+ - queries/ Фабрики запросов
+ - example.ts
+ - another-example.ts
+
+
+ ```ts title="src/shared/api/index.ts"
+ export { exampleQueries } from './queries/example';
+ ```
+
+
+
+
+
+ Если объём эндпоинтов оказывается достаточно большим и Вы также хотите хранить их в `shared`, то лучше разбить всё по контроллерам и использовать публичный API для каждого из них
+
+
+ - src/
+ - app/
+ - pages/
+ - widgets/
+ - features/
+ - entities/
+ - shared/
+ - api/
+ - example/
+ - index.ts
+ - example.query.ts Фабрика запросов, где определены ключи и функции для контроллера example
+ - get-example.ts
+ - create-example.ts
+ - update-example.ts
+ - delete-example.ts
+ - another-example/
+ - index.ts
+ - another-example.query.ts Фабрика запросов, где определены ключи и функции для контроллера another-example
+ - get-another-example.ts
+ - create-another-example.ts
+ - update-another-example.ts
+ - delete-another-example.ts
+
+
+ ```ts title="src/shared/api/example/index.ts"
+ export { exampleQueries } from "./example.query";
+ ```
+
+
+
+
+
+ *Если в проекте уже присутствует разделение на сущности*, и каждый запрос соответствует одной сущности,
+ наиболее чистым будет разделение по сущностям. В таком случае, предлагаем использовать следующую структуру:
+
+
+ - src/
+ - app/
+ - pages/
+ - widgets/
+ - features/
+ - entities/
+ - example/
+ - api/
+ - example.query.ts Фабрика запросов, где определены ключи и функции
+ - get-example.ts
+ - create-example.ts
+ - update-example.ts
+ - delete-example.ts
+ - shared/
+
+
+ Если среди сущностей есть связи (например, у сущности Страна есть поле-список сущностей Город), то можно воспользоваться [публичным API для кросс-импортов][public-api-for-cross-imports].
+
+
+
+
+
+## Где располагать мутации \{#where-to-store-mutations\}
+
+Мутации не рекомендуется смешивать с запросами. Исходя из этого можно предложить несколько вариантов:
+
+
+
+
+
+ ```tsx title="src/pages/example/api/use-update-example.ts"
+ export const useUpdateExample = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ id, newTitle }) => {
+ const { data } = await apiClient.patch(`/posts/${ id }`, { title: newTitle });
+
+ return data;
+ },
+ onSuccess: newPost => {
+ queryClient.setQueryData(postsQueries.ids(id), newPost);
+ },
+ });
+ };
+ ```
+
+
+
+
+
+ ```tsx title="src/pages/example/ui/example.tsx"
+ export const Example = () => {
+ const [title, setTitle] = useState('');
+
+ const { mutate, isPending } = useMutation({
+ mutationFn: mutations.createExample,
+ });
+
+ const handleChange = ({ target: { value } }: ChangeEvent) => {
+ setTitle(value);
+ };
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ mutate({ title, userId: DEFAULT_USER_ID });
+ };
+
+ return (
+
+ );
+ };
+ ```
+
-## Проблема «куда положить ключи»
+
-### Решение - разбить по сущностям
+## Организация запросов \{#organizing-queries\}
-Если в проекте уже присутствует разделение на сущности, и каждый запрос соответствует одной сущности,
-наиболее чистым будет разделение по сущностям. В таком случае, предлагаем использовать следующую структуру:
+### Фабрика запросов \{#query-factory\}
-
-- src/
- - app/
- - pages/
- - entities/
- - \{entity\}/
- - api/
- - \{entity\}.query (Фабрика запросов, где определены ключи и функции)
- - get-\{entity\} (Функция получения сущности)
- - create-\{entity\} (Функция создания сущности)
- - update-\{entity\} (Функция обновления объекта)
- - delete-\{entity\} (Функция удаления объекта)
- - features/
- - widgets/
- - shared/
-
+В этом гайде рассмотрим, как использовать фабрику запросов (объект, где значениями ключа - являются функции, возвращающие список ключей запроса)
-Если среди сущностей есть связи (например, у сущности Страна есть поле-список сущностей Город), то можно воспользоваться
-[публичным API для кросс-импортов][public-api-for-cross-imports] или рассмотреть альтернативное решение ниже.
+```ts
+const keyFactory = {
+ all: () => ["entity"],
+ lists: () => [...postQueries.all(), "list"],
+};
+```
+:::info
+ `queryOptions` - встроенная утилита react-query@v5
-### Альтернативное решение — хранить запросы в общем доступе.
+Одним из лучших способов совместного использования `queryKey` и `queryFn` в нескольких местах — это использование вспомогательной функции `queryOptions` [(подробнее здесь)](https://tanstack.com/query/latest/docs/framework/react/guides/query-options)
-В случаях, когда не подходит разделение по сущностям, можно рассмотреть следующую структуру:
+```ts
+import { queryOptions } from '@tanstack/react-query';
-
-- src/
- - shared/
- - api/
- - queries (Query-factories)
- - document.ts
- - background-jobs.ts
- - index.ts
-
+const groupOptions = (id: number) => queryOptions({
+ queryKey: ['groups', id],
+ queryFn: () => fetchGroups(id),
+ gcTime: 5 * 1000,
+});
+```
+:::
+
+### 1. Создание Фабрики запросов \{#creating-query-factory\}
+
+```tsx title="src/shared/api/post/post.queries.ts"
+import { queryOptions } from '@tanstack/react-query';
+import { getPosts } from './get-posts';
+import { getDetailPost, type DetailPostQuery } from './get-detail-post';
+
+export const POST_QUERIES = {
+ all: () => ['posts'],
+ lists: () => [...POST_QUERIES.all(), 'list'],
+ list: (page: number, limit: number) => queryOptions({
+ queryKey: [...POST_QUERIES.lists(), page, limit],
+ queryFn: () => getPosts(page, limit),
+ placeholderData: prev => prev,
+ }),
+ details: () => [...POST_QUERIES.all(), 'detail'],
+ detail: (query?: DetailPostQuery) => queryOptions({
+ queryKey: [...POST_QUERIES.details(), query?.id],
+ queryFn: () => getDetailPost({ id: query?.id }),
+ }),
+};
+```
-Затем в `@/shared/api/index.ts`:
+### 2. Применение Фабрики запросов в коде приложения \{#using-query-factory\}
-```ts title="@/shared/api/index.ts"
-export { documentQueries } from "./queries/document";
-```
+```tsx title="src/pages/post/ui/post.tsx"
+import { useParams } from 'react-router';
+import { postApi } from '@/shared/api/post';
+import { useQuery } from '@tanstack/react-query';
-## Проблема «Куда мутации?»
+interface Params {
+ postId: string;
+}
-Мутации не рекомендуется смешивать с запросами. Возможны два варианта:
+export const Post = () => {
+ const { postId } = useParams();
-### 1. Определить кастомный хук в сегменте api рядом с местом использования
+ const {
+ data: post,
+ error,
+ isLoading,
+ isError
+ } = useQuery(postApi.postQueries.detail({ id: parseInt(postId ?? '', 10) }));
-```tsx title="@/features/update-post/api/use-update-title.ts"
-export const useUpdateTitle = () => {
- const queryClient = useQueryClient();
+ if (isLoading) {
+ return (
+