Client State
Определения
Client State
Client State - это состояние, описывающее поведение интерфейса и пользовательские сценарии.
Ключевые свойства Client State:
- Полностью контролируется клиентом
- Источник истины - клиент
- Не устаревает само по себе
Управление Client State
Нужно ли вообще глобальное состояние?
MUST
По умолчанию используется стандартный поток данных React:
- Локальное состояние компонентов (
useState,useReducer) - Передача данных через
props - Изменение состояния через callbacks
State manager нужен, если
- Состояние используется в компонентах, не связанных напрямую иерархией
- "Поднять состояние выше" приводит к разрастанию компонентов и смешиванию ответственности
- Состояние относится к уровню приложения, а не конкретного UI-элемента
Если есть сомнения - глобальное состояние вводить не нужно.
Инструменты Client State
Разрешенн ые библиотеки
Допустимые библиотеки:
- Redux Toolkit
- Zustand
Оба инструмента валидны, но предназначены для разных сценариев.
Как выбрать инструмент для Client State
1) Локально для одного компонента/поддерева
Используем React локально:
useStateuseReducer
// ✅ Хорошо
function Filters() {
const [isOpen, setIsOpen] = useState(false);
return <button onClick={() => setIsOpen(v => !v)}>Toggle</button>;
}
2) Глобально, но простое и ограниченное по области
Используем Zustand (bounded context)
3) Глобально, сложное и критична предсказуемость
Используем Redux Toolkit
Redux Toolkit
Когда подходит
Redux Toolkit подходит, если:
- Логика переходов состояния сложная
- Есть инв арианты и зависимые изменения
- Важна предсказуемость и воспроизводимость
- Ожидается рост проекта/команды
- Нужна "дисциплина по умолчанию" (архитектурные ограничения)
Правила
MUST
- Store — единственный источник истины
- Состояние разделено на slices
- 1 slice = 1 зона ответственности
- UI работает через:
- Селекторы
- и/или доменные хуки
- UI не знает внутреннюю структуру хранилища
Побочные эффекты
MUST
- Reducers — чистые функции
- Side effects выносятся в:
- Thunks
- Middleware
- Orchestration слой на уровне feature
✅ Хорошо (slice + selectors)
// features/auth/model/auth-slice.ts
import { createSlice, createSelector } from '@reduxjs/toolkit';
interface AuthState {
isAuthorized: boolean;
userId?: string;
}
const initialState: AuthState = { isAuthorized: false };
const slice = createSlice({
name: 'auth',
initialState,
reducers: {
loggedIn: (state, action) => {
state.isAuthorized = true;
state.userId = action.payload.userId;
},
loggedOut: (state) => {
state.isAuthorized = false;
state.userId = undefined;
},
},
});
export const authReducer = slice.reducer;
export const authActions = slice.actions;
export const selectAuth = (root: RootState) => root.auth;
export const selectIsAuthorized = createSelector(
selectAuth,
(s) => s.isAuthorized
);
Zustand
Когда подходит
Zustand подходит, если:
- Состояние простое
- Логики немного
- Область применения можно жёстко ограничить (bounded state contexts)
- Нет необходимости в сложной модели переходов состояния
Zustand требует архитектурных ограничений, иначе быстро превращается в неуправляемое глобальное состояние.
Правила Zustand
FORBIDDEN
- Глобальный singleton-store, доступный из любой точки приложения
Допустимые модели
Модель A: Изолированное хранилище через React Context (рекомендуется)
✅ Хорошо: наружу экспортируем только Provider и доменные хуки, сам store-экземпляр не экспортируем.
// features/checkout/model/checkoutStore.ts
import { createContext, useContext, ReactNode } from 'react';
import { createStore } from 'zustand/vanilla';
import { useStore } from 'zustand';
interface CheckoutState {
step: 'cart' | 'delivery' | 'payment';
setStep: (step: CheckoutState['step']) => void;
}
const createCheckoutStore = () =>
createStore<CheckoutState>((set) => ({
step: 'cart',
setStep: (step) => set({ step })
}));
const CheckoutStoreContext = createContext<ReturnType<typeof createCheckoutStore> | null>(null);
export function CheckoutStoreProvider({ children }: { children: ReactNode }) {
const store = createCheckoutStore();
return (
<CheckoutStoreContext.Provider value={store}>
{children}
</CheckoutStoreContext.Provider>
);
}
export function useCheckoutStep() {
const store = useContext(CheckoutStoreContext);
if (!store) {
throw new Error('CheckoutStoreProvider is missing');
}
return useStore(store, (s) => s.step);
}
export function useCheckoutActions() {
const store = useContext(CheckoutStoreContext);
if (!store) {
throw new Error('CheckoutStoreProvider is missing');
}
return useStore(store, (s) => ({ setStep: s.setStep }));
}
Модель B: Ограниченное "общее" хранилище (если нужно)
MUST
- Запрет доступа к root state напрямую
- У каждого логического блока свой Public API (селекторы/хуки)