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

Управление Server State

Определение

Server State - это снимок (snapshot) данных, полученных с сервера в конкретный момент времени.

Ключевые свойства Server State:

  • данные могут устаревать
  • данные могут изменяться вне клиента
  • клиент не является источником истины

Цель слоя Server State

Слой Server State отвечает за:

  • управление жизненным циклом серверных данных (fetch/refetch/retry)
  • кеширование и дедупликацию запросов
  • контроль актуальности данных (staleTime/cacheTime)
  • синхронизацию данных с UI

Библиотеки

Для управления Server State используем специализированные библиотеки:

  • TanStack Query
  • SWR
  • RTK Query (в связке с Redux Toolkit)

Запрещено хранить 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

Примеры

1.1 ✅ Хорошо

// 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 });
}

1.2 ✅ Хорошо (query factory)

// entities/user/api/user.queries.ts
import { fetchUserById } from './user.api'; // импорт API-метода для получения данных о пользователе (пример #1)

export const userQuery = (userId: string) => ({
queryKey: ['user', userId] as const,
queryFn: ({ signal }: { signal?: AbortSignal }) => fetchUserById(userId, signal)
});

1.3 ❌ Плохо (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 напрямую

Примеры

2.1 ✅ Хорошо (domain hook)

// entities/user/model/useUserQuery.ts
import { useQuery } from '@tanstack/react-query';
import { userQuery } from '../api/user.queries'; // (пример #1.2 импортируем query factory)

export function useUserQuery(userId: string) {
return useQuery({
...userQuery(userId),
staleTime: 60_000 // 1 минута для редко меняющихся данных
});
}

2.2 ✅ Хорошо (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
      • queryKey factories

3.1 ✅ Хорошо

// entities/user/index.ts
export { useUserQuery } from './model/useUserQuery';
export { useUpdateUserMutation } from './model/useUpdateUserMutation';
export { invalidateUserRelatedQueries } from './model/cache';

3.2 ❌ Плохо

// entities/user/index.ts
export { userQuery } from './api/user.queries'; // наружу keys/query factories не отдаем
export { USER_ROOT_KEY } from './api/keys'; // запрещено

Семантические helper-функции для инвалидации (обязательный паттерн)

Если другому модулю нужно повлиять на Server State текущего модуля (например выполнить инвалидацию), текущий модуль обязан предоставить helper-функцию, скрывающую детали кеша.

  1. ✅ Хорошо
// 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)
});
}

Почему это правильно:

  • внешний код не знает, какие ключи используются
  • внешний код не знает, сколько их
  • внешний код оперирует доменной семантикой, а не структурой кеша
  1. ❌ Плохо (внешний код инвалидирует по ключам напрямую)
queryClient.invalidateQueries({ queryKey: ['user', userId] }); // ависимость от деталей кеша

Best Practices

Cache keys & query factories

MUST

  • cache keys и query factories централизованы внутри модуля
  • inline keys не используются (кроме MVP-исключений)

Примеры:

  1. ✅ Хорошо
// entities/order/api/order.keys.ts
export const orderKeys = {
root: ['order'] as const,
byId: (id: string) => ['order', 'byId', id] as const
};
  1. ❌ Плохо
useQuery({ queryKey: ['order', id], queryFn: () => null }); // inline

Stale time

SHOULD

  • для редко изменяющихся данных staleTime 1–5 минут

Не допускается

FORBIDDEN

  • хранить Server State в Zustand/MobX и других client-state менеджерах
  • напрямую работать с внутренним кешем библиотеки без веской причины
  • экспортировать cache keys/query factories как часть Public API
  • импортировать query keys между модулями

Временные отклонения (MVP / small projects)

Допускается (временно)

  • использование useQuery/useMutation напрямую в UI
  • отсутствие строгого разделения entities//api и entities//model
  • inline query keys в пределах одного модуля

Обязательные ограничения

  • Server State не хранится в client state менеджерах
  • данные не дублируются между слоями
  • UI не начинает владеть жизненным циклом серверных данных

Когда возвращаемся к основным правилам

  • появляются межмодульные зависимости
  • нужна инвалидация из других частей приложения
  • растёт количество запросов и сценариев

Процедуры контроля (Code Review)

Ревьюер должен убедиться, что:

  • нет импортов fetch/axios вне shared/api
  • нет импортов useQuery/useMutation вне entities//model и features//model (кроме MVP)
  • Server State не хранится в Zustand/MobX
  • управление Server State осуществляется через domain-хуки
  • cache keys/query factories не экспортируются из модуля наружу
  • нет импортов query keys между модулями
  • инвалидация/оптимистические апдейты из других модулей выполняются только через семантические helper-функции

Автоматизация (lint / boundary checks)

MUST

Должны быть настроены проверки, запрещающие:

  • импорт библиотек Server State (TanStack Query/SWR/RTK Query) из UI-слоя
  • импорт cache keys/query factories за пределы доменного модуля
  • импорт fetch/axios вне shared/api

Реализация: eslint rules + boundary checks (например eslint-plugin-boundaries) или кастомные правила.