Перейти к основному содержимому

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

  • Для редко изменяющихся данных staleTime 1–5 минут
// Редко меняющиеся данные (настройки, справочники)
staleTime: 5 * 60 * 1000; // 5 минут

// Часто меняющиеся данные (корзина, уведомления)
staleTime: 0; // всегда refetch