Урок 05

Pinia vs Redux/Zustand

Задача

Глобальное состояние — когда компоненты далеко, но должны видеть одни данные.

  • Vue → Pinia
  • React → Redux или Zustand

Redux (классика)

store/counterSlice.ts:

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

const slice = createSlice({
    name: 'counter',
    initialState: { value: 0 },
    reducers: {
        increment: (state) => { state.value++; },
        reset: (state) => { state.value = 0; },
    },
});

export const { increment, reset } = slice.actions;
export default slice.reducer;

Компонент:

const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();

dispatch(increment());

Много кода. Actions, reducers, dispatch.

Zustand (современный)

import { create } from 'zustand';

const useCounterStore = create((set) => ({
    count: 0,
    increment: () => set(s => ({ count: s.count + 1 })),
    reset: () => set({ count: 0 }),
}));

Компонент:

const count = useCounterStore(s => s.count);
const increment = useCounterStore(s => s.increment);

increment();

Намного проще Redux.

Pinia

// stores/counter.ts
export const useCounterStore = defineStore('counter', () => {
    const count = ref(0);

    function increment() {
        count.value++;
    }

    function reset() {
        count.value = 0;
    }

    return { count, increment, reset };
});

Компонент:

<script setup lang="ts">
const counter = useCounterStore();
</script>

<template>
    <button @click="counter.increment">{{ counter.count }}</button>
</template>

Сравнение синтаксиса

Pinia — похож на Zustand, не на Redux.

  • Pinia ≈ Zustand
  • Redux — legacy, но распространён

State и Actions

Pinia:

const count = ref(0);
const doubled = computed(() => count.value * 2);

function increment() { count.value++; }

return { count, doubled, increment };

Zustand:

const useStore = create((set, get) => ({
    count: 0,
    get doubled() { return get().count * 2; },
    increment: () => set(s => ({ count: s.count + 1 })),
}));

Pinia проще — нет set/get.

Redux:

const slice = createSlice({
    name: 'counter',
    initialState: { count: 0 },
    reducers: {
        increment: (state) => { state.count++; },
    },
});

Асинхронные actions

Pinia:

async function loadUsers() {
    loading.value = true;
    try {
        users.value = await api.getUsers();
    } finally {
        loading.value = false;
    }
}

Zustand:

const loadUsers = async () => {
    set({ loading: true });
    try {
        const users = await api.getUsers();
        set({ users });
    } finally {
        set({ loading: false });
    }
};

Redux — через createAsyncThunk:

export const loadUsers = createAsyncThunk('users/load', async () => {
    return await api.getUsers();
});

Redux сложнее всех.

DevTools

Все три дружат с DevTools:

  • Pinia — Vue DevTools
  • Redux — Redux DevTools (эталон)
  • Zustand — Redux DevTools (через middleware)

Redux мощнее — time-travel, replay.

Модульность

Pinia:

// stores/user.ts
export const useUserStore = defineStore('user', () => { ... });

// stores/cart.ts
export const useCartStore = defineStore('cart', () => { ... });

Zustand:

// stores/user.ts
export const useUserStore = create(...);

// stores/cart.ts
export const useCartStore = create(...);

Redux:

const store = configureStore({
    reducer: {
        user: userReducer,
        cart: cartReducer,
    },
});

Сторы в сторах

Pinia:

export const useOrderStore = defineStore('order', () => {
    const user = useUserStore();
    const cart = useCartStore();

    function checkout() {
        if (!user.isLoggedIn) throw new Error();
        api.checkout(cart.items);
    }

    return { checkout };
});

Zustand — через get():

const useOrderStore = create((set, get) => ({
    checkout: () => {
        const user = useUserStore.getState();
        if (!user.isLoggedIn) throw new Error();
    },
}));

Кеширование

Pinia + Persist:

npm install pinia-plugin-persistedstate
export const useCartStore = defineStore('cart', () => { ... }, {
    persist: true,
});

Zustand + persist:

import { persist } from 'zustand/middleware';

const useStore = create(persist(
    (set) => ({ ... }),
    { name: 'cart' }
));

Размер

Размер
Pinia~1.5 КБ
Zustand~1 КБ
Redux Toolkit~13 КБ

Когда что

Pinia:

  • Vue 3 проекты
  • Малые и средние
  • Нужна простота

Zustand:

  • React
  • Малые и средние
  • Простота важнее DevTools

Redux:

  • React
  • Крупные проекты
  • Много команд
  • Нужна строгая структура

Полный пример — корзина

Pinia:

export const useCartStore = defineStore('cart', () => {
    const items = ref<CartItem[]>([]);
    const total = computed(() =>
        items.value.reduce((s, i) => s + i.price * i.qty, 0)
    );

    function add(product: Product) {
        const existing = items.value.find(i => i.id === product.id);
        if (existing) existing.qty++;
        else items.value.push({ ...product, qty: 1 });
    }

    return { items, total, add };
});

Zustand:

export const useCartStore = create((set, get) => ({
    items: [] as CartItem[],
    get total() {
        return get().items.reduce((s, i) => s + i.price * i.qty, 0);
    },
    add: (product: Product) => set(s => {
        const existing = s.items.find(i => i.id === product.id);
        if (existing) {
            return {
                items: s.items.map(i =>
                    i.id === product.id ? { ...i, qty: i.qty + 1 } : i
                ),
            };
        }
        return { items: [...s.items, { ...product, qty: 1 }] };
    }),
}));

Zustand длиннее из-за иммутабельности.

Итог

  • Pinia ≈ Zustand по простоте
  • Redux — тяжелее, но мощнее DevTools
  • Pinia — по умолчанию для Vue
  • Zustand — для React
  • Redux — если уже есть в проекте

Дальше — загрузка данных: useFetch.