Урок 07

Глобальное состояние

Когда состояние нужно многим экранам, используют стор.

Когда нужно

  • Пользователь (авторизация)
  • Тема
  • Корзина
  • Настройки приложения
  • Кеш избранного

Варианты

  • Zustand — минималистичный
  • Redux Toolkit — мощный, но много boilerplate
  • Jotai — атомарный
  • Context + useReducer — без библиотек

Zustand

npm install zustand
import { create } from 'zustand';

type CartStore = {
    items: CartItem[];
    add: (item: CartItem) => void;
    remove: (id: number) => void;
    clear: () => 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) })),
    clear: () => set({ items: [] }),
    total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));

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

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

function CartBadge() {
    const count = useCartStore((s) => s.items.length);
    return <Text>{count}</Text>;
}

Persist в Zustand

import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

export const useCartStore = create<CartStore>()(
    persist(
        (set, get) => ({
            // ...
        }),
        {
            name: 'cart',
            storage: createJSONStorage(() => AsyncStorage),
        }
    )
);

Корзина сохраняется между запусками.

Redux Toolkit

npm install @reduxjs/toolkit react-redux
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 { Provider, useSelector, useDispatch } from 'react-redux';

<Provider store={store}>
    <App />
</Provider>

function Cart() {
    const items = useSelector((s: RootState) => s.cart.items);
    const dispatch = useDispatch();
    return <>{items.map((i) => <Text key={i.id}>{i.name}</Text>)}</>;
}

Context + useReducer

Без библиотек:

const CartStateContext = createContext<CartItem[] | null>(null);
const CartDispatchContext = createContext<Dispatch<Action> | null>(null);

function CartProvider({ children }: { children: React.ReactNode }) {
    const [state, dispatch] = useReducer(reducer, []);

    return (
        <CartStateContext.Provider value={state}>
            <CartDispatchContext.Provider value={dispatch}>
                {children}
            </CartDispatchContext.Provider>
        </CartStateContext.Provider>
    );
}

Что выбрать

  • Zustand — 80% случаев, минималистичный
  • Redux Toolkit — большой проект, devtools, middleware
  • Jotai — атомарное состояние, много независимых частей
  • Context — простые случаи, одна-две переменных

Серверное состояние ≠ глобальное

Серверные данные — в TanStack Query. Глобальное состояние — только клиентское: корзина, тема, настройки.

Итоги

  • Zustand для большинства случаев
  • Redux для энтерпрайза
  • Persist для сохранения
  • Context без библиотек
  • Серверное состояние — в TanStack Query