Лучшие практики и контроль
Разделение Server State и Client State
MUST
В приложении обязано быть чёткое разделение:
Server State (управляется TanStack Query / SWR / RTK Query)
- Данные из API, которые могут устаревать/меняться вне клиента
- Примеры: пользователи, заказы, продукты, настройки из API
Client State (управляется Redux Toolkit / Zustand)
- Синхронное состояние UI и пользовательских сценариев
- Примеры: открыт/закрыт modal, выбранный tab, step в wizard, фильтры
То, что технически и data, и state можно хранить в state-менеджерах, не означает, что так следует делать.
FORBIDDEN
- Хранить Server State в Redux/Zustand (данные из API, сущности,
isLoading,error,lastFetchedAtи т.д.) - Дублировать одно и то же состояние между Server State и Client State
❌ Плохо
// НЕ храним сущности из API в zustand/redux
const useStore = create(() => ({
users: [], // ❌ Server State
isLoadingUsers: false, // ❌ Server State
usersError: null, // ❌ Server State
}));
✅ Хорошо
// Server State - в TanStack Query
const {
data: users,
isLoading,
error,
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
// Client State - только UI/flow
const useUIStore = create(() => ({
selectedTab: 'active', // ✅ Client State
isFiltersOpen: false, // ✅ Client State
currentStep: 1, // ✅ Client State
}));
Где должно жить состояние
MUST
- Состояние локально для одного UI-сценария → React local state
- Состояние связано с серверными данными → Server State library (TanStack Query)
- Состояние поведения/пользовательских сценариев → Client State (Redux/Zustand)
- Derived state → селекторы/функции, а не хранилище
Bad Practices
1) Server State в Redux/Zustand
❌ Плохо
// ❌ НЕ храним сущности из API в zustand
const useStore = create(() => ({
users: [],
isLoadingUsers: false,
usersError: null,
}));
✅ Хорошо
// ✅ Server State - в TanStack Query
const { data: users, isLoading, error } = useUsersQuery();
// ✅ Client State - только UI/flow
const useUIStore = create(() => ({
selectedTab: 'active',
isFiltersOpen: false,
}));
2) Хранилище как "шина событий" (commands вместо facts)
❌ Плохо
// ❌ shouldRedirectToDashboard - это команда, а не факт
const slice = createSlice({
name: 'auth',
initialState: { shouldRedirectToDashboard: false },
reducers: {
loginSuccess: (state) => {
state.shouldRedirectToDashboard = true;
},
redirectDone: (state) => {
state.shouldRedirectToDashboard = false;
},
},
});
✅ Хорошо (храним факт)
// ✅ Факт: пользователь авторизован
// UI сам решает, что делать в своём сценарии
const slice = createSlice({
name: 'auth',
initialState: { isAuthorized: false },
reducers: {
loginSuccess: (state) => {
state.isAuthorized = true;
},
},
});
3) Derived state в хранилище
❌ Плохо
// ❌ items + total - два источника истины
const useCartStore = create(() => ({
items: [],
total: 0,
}));
✅ Хорошо (селектор/функция)
// ✅ Total вычисляется на лету
export const selectCartTotal = (items: CartItem[]) =>
items.reduce((sum, i) => sum + i.price * i.qty, 0);
Временные отклонения (MVP / small projects)
Допускается (временно)
Для Server State:
- Использование
useQuery/useMutationнапрямую в UI - Отсутствие строгого разделения
entities/*/apiиentities/*/model - Inline query keys в пределах одного модуля
Для Client State:
- Один простой глобальный store
- Менее строгая композиция
- Ограниченное смешение UI-состояния и сценариев
Обязательные ограничения (всё равно MUST)
Для Server State:
- Server State не хранится в client state менеджерах
- Данные не дублируются между слоями
- UI не начинает владеть жизненным циклом серверных данных
Для Client State:
- Client State не хранит Server State
- Состояние не используется как "event bus"
- Derived state не хранится
- Один источник истины сохраняется
Когда возвращаемся к основным правилам
Для Server State:
- Появляются межмодульные зависимости
- Нужна инвалидация из других частей приложения
- Растёт количество запросов и сценариев
Для Client State:
- Рост количества UI-сценариев
- Усложнение переходов состояния
- Подключение Redux Toolkit или расширение Zustand
Процедуры контроля (Code Review)
Ревьюер должен убедиться, что
Общие требования
- ✅ Чёткое разделение Server State и Client State
- ❌ Server State не хранится в Redux/Zustand
- ❌ Client/Server state не дублируются
- ✅ У каждого куска состояния ровно один источник истины
Server State
- ❌ Нет импортов fetch/axios вне
shared/api - ❌ Нет импортов useQuery/useMutation вне
entities/*/modelиfeatures/*/model(кроме MVP) - ✅ Управление Server State осуществляется через domain-хуки
- ❌ Cache keys/query factories не экспортируются из модуля наружу
- ❌ Нет импортов query keys между модулями
- ✅ Инвалидация/оптимистические апдейты из других модулей выполняются только через семантические helper-функции
Client State
- ✅ State manager используется осознанно, а не "по умолчанию"
- ❌ Локальное состояние не вынесено в глобальное без причины
- ❌ Состояние не используется для передачи команд/намерений (event bus)
- ✅ Логика изменения состояния чистая
- ✅ Side effects вынесены в thunks/middleware/orchestration слой feature
- ❌ UI не импортирует внутренности хранилищ
- ✅ Доступ к состоянию осуществляется через:
- Селекторы
- Доменные хуки
- Public API модуля
Автоматизация (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) или кастомные правила.