Server State
Определения
Server State
Server State - это снимок (snapshot) данных, полученных с сервера в конкретный момент времени.
Ключевые свойства Server State:
- Данные могут устаревать
- Данные могут изменяться вне клиента
- Клиент не является источником истины
Управление Server State
Цель слоя Server State
Слой Server State отвечает за:
- Управление жизненным циклом серверных данных (fetch/refetch/retry)
- Кеширование и дедупликацию запросов
- Контроль актуальности данных (staleTime/cacheTime)
- Синхронизацию данных с UI
Библиотеки для Server State
MUST
Для управления Server State используем специализированные библиотеки:
Допустимые библиотеки:
- TanStack Query (рекомендуется)
- SWR
- RTK Query (в связке с Redux Toolkit)
FORBIDDEN
Запрещено хранить Server State в Zustand/MobX и т.п. - это client state менеджеры.
Архитектура в FSD
В рамках Feature-Sliced Design код, связанный с запросами, локализуется н а уровне доменного модуля, но разделяется по ответственности.
Слои и ответственность
1) entities/*/api и features/*/api
Назначение: описывать контракт доступа к серверу.
MUST:
- Функции вызова API (обертки над
shared/api) - Query factories (
queryKey+queryFn) - Описание endpoint'ов сущности/фичи
FORBIDDEN:
- React-код
- Управление жизненным циклом (refetch/retry policies)
- Cache invalidation логика
- Работа с QueryClient
Примеры
✅ Хорошо (API метод)
// entities/user/api/user.api.ts
import { http } from '@/shared/api/http';
import type { UserDto } from '@/shared/types/dto';
export function fetchUserById(userId: string, signal?: AbortSignal) {
return http.get<UserDto>(`/users/${userId}`, { signal });
}
✅ Хорошо (query factory)
// entities/user/api/user.queries.ts
import { fetchUserById } from './user.api';
export const userQuery = (userId: string) => ({
queryKey: ['user', userId] as const,
queryFn: ({ signal }: { signal?: AbortSignal }) =>
fetchUserById(userId, signal),
});
❌ Плохо (invalidation в api-слое)
// entities/user/api/user.queries.ts
queryClient.invalidateQueries({ queryKey: ['user'] }); // ❌ нельзя в api
2) entities/*/model и features/*/model
Назначение: слой model - владелец Server State доменного модуля.
MUST:
- Domain-хуки (
useXQuery,useYMutation) - Retry policy, staleTime/cacheTime
- Логика инвалидации/оптимистических апдейтов
- Семантические helper-функции для управления Server State (например
invalidateUserRelatedQueries)
FORBIDDEN:
- UI-слой не использует
useQuery/useMutationнапрямую - Внешний код не знает cache keys и структуру queryKey
- В нешний код не делает invalidation напрямую
Примеры
✅ Хорошо (domain hook)
// entities/user/model/useUserQuery.ts
import { useQuery } from '@tanstack/react-query';
import { userQuery } from '../api/user.queries';
export function useUserQuery(userId: string) {
return useQuery({
...userQuery(userId),
staleTime: 60_000, // 1 минута для редко меняющихся данных
});
}
✅ Хорошо (mutation + invalidation внутри модуля)
// entities/user/model/useUpdateUserMutation.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateUser } from '../api/updateUser.api';
import { invalidateUserRelatedQueries } from './cache';
export function useUpdateUserMutation(userId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: (dto: UpdateUserDto) => updateUser(userId, dto),
onSuccess: () => invalidateUserRelatedQueries(qc, userId),
});
}
3) Public API модуля (entities/user/index.ts)
MUST
Public API может экспортировать:
- Domain-хуки
- Семантические helper-функции
Public API не экспортирует:
- Cache keys
- Query factories
✅ Хорошо
// entities/user/index.ts
export { useUserQuery } from './model/useUserQuery';
export { useUpdateUserMutation } from './model/useUpdateUserMutation';
export { invalidateUserRelatedQueries } from './model/cache';
❌ Плохо
// entities/user/index.ts
export { userQuery } from './api/user.queries'; // ❌ наружу keys/query factories не отдаем
export { USER_ROOT_KEY } from './api/keys'; // ❌ запрещено
Се мантические helper-функции для инвалидации (обязательный паттерн)
Принцип
Если другому модулю нужно повлиять на Server State текущего модуля (например выполнить инвалидацию), текущий модуль обязан предоставить helper-функцию, скрывающую детали кеша.
✅ Хорошо
// entities/user/model/cache.ts
import type { QueryClient } from '@tanstack/react-query';
const USER_ROOT_KEY = 'user';
export function invalidateUserRelatedQueries(
queryClient: QueryClient,
userId: string
) {
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === USER_ROOT_KEY && query.queryKey.includes(userId),
});
}
Почему это правильно:
- Внешний код не знает, какие ключи используются
- Внешний код не знает, сколько их
- Внешний код опер ирует доменной семантикой, а не структурой кеша
❌ Плохо (внешний код инвалидирует по ключам напрямую)
// ❌ Зависимость от деталей кеша
queryClient.invalidateQueries({ queryKey: ['user', userId] });
Best Practices для Server State
Cache keys & query factories
MUST
- Cache keys и query factories централизованы внутри модуля
- Inline keys не используются (кроме MVP-исключений)
✅ Хорошо
// entities/order/api/order.keys.ts
export const orderKeys = {
root: ['order'] as const,
byId: (id: string) => ['order', 'byId', id] as const,
};
❌ Плохо
// ❌ Inline key
useQuery({ queryKey: ['order', id], queryFn: () => null });
Stale time
SHOULD
- Для редко изменяющихся данных
staleTime1–5 минут
// Редко меняющиеся данные (настройки, справочники)
staleTime: 5 * 60 * 1000; // 5 минут
// Часто меняющиеся данные (корзина, уведомления)
staleTime: 0; // всегда refetch