Урок 06

Когда нужен глобальный стор

Локальный стейт и контекст покрывают большинство случаев. Но иногда нужен глобальный стор — централизованное хранилище состояния.

Проблемы контекста

  • Каждое изменение значения перерисовывает всех потребителей
  • Сложно организовать несколько независимых кусков состояния
  • Нет инструментов для отладки: devtools, time-travel
  • Нет middleware для логирования, асинхронности

Что даёт стор

  • Селекторы — компонент подписывается только на нужную часть состояния
  • DevTools — просмотр истории изменений
  • Middleware — логирование, асинхронные действия
  • Модульность — несколько независимых слайсов
  • Производительность — точечные обновления

Пример: Zustand

import { create } from 'zustand';

type CartStore = {
    items: CartItem[];
    add: (item: CartItem) => void;
    remove: (id: number) => void;
    total: () => number;
};

export const useCartStore = create<CartStore>((set, get) => ({
    items: [],
    add: (item) => set((s) => ({ items: [...s.items, item] })),
    remove: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
    total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));

Использование:

function CartButton({ item }: { item: CartItem }) {
    const add = useCartStore((s) => s.add);
    return <button onClick={() => add(item)}>В корзину</button>;
}

function CartTotal() {
    const total = useCartStore((s) => s.total());
    return <p>Итого: {total}</p>;
}

useCartStore((s) => s.add) — селектор. Компонент перерисовывается только при изменении выбранной части.

Redux Toolkit

Классический Redux. Больше boilerplate, но мощнее.

import { createSlice, configureStore } from '@reduxjs/toolkit';

const cartSlice = createSlice({
    name: 'cart',
    initialState: { items: [] as CartItem[] },
    reducers: {
        add: (state, action) => {
            state.items.push(action.payload);
        },
        remove: (state, action) => {
            state.items = state.items.filter((i) => i.id !== action.payload);
        },
    },
});

export const { add, remove } = cartSlice.actions;

export const store = configureStore({
    reducer: { cart: cartSlice.reducer },
});

В компоненте:

import { useSelector, useDispatch } from 'react-redux';

function Cart() {
    const items = useSelector((s: RootState) => s.cart.items);
    const dispatch = useDispatch();

    return (
        <ul>
            {items.map((item) => (
                <li key={item.id}>
                    {item.name}
                    <button onClick={() => dispatch(remove(item.id))}>Удалить</button>
                </li>
            ))}
        </ul>
    );
}

Другие сторы

  • Jotai — атомарный подход, минимальный boilerplate
  • Valtio — прокси-объекты, мутируемый синтаксис
  • MobX — реактивный, классы и декораторы

Когда нужен стор

  • Данные нужны во многих местах приложения
  • Данные меняются часто
  • Нужна история изменений и devtools
  • Нужны middleware (логирование, API)
  • Состояние большое и структурированное

Когда не нужен

  • Данные локальные для одного компонента
  • Данные нужны небольшой группе компонентов — контекст
  • Серверные данные — TanStack Query / SWR

Серверные данные — отдельно

Серверные данные (списки, детали, пагинация) лучше держать в TanStack Query или SWR. Они кешируют, инвалидируют, повторяют запросы.

Стор — для клиентского состояния: тема, корзина, модалки, фильтры UI.

Итоги

  • Стор — для глобального состояния
  • Селекторы дают точечные обновления
  • Zustand — минималистичный, Redux — мощный
  • Не всё состояние нужно в сторе
  • Серверные данные — отдельный слой