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.