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

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 локально:

  • useState
  • useReducer
// ✅ Хорошо
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 (селекторы/хуки)