17. Vuex 4 (legacy)
📦 Vuex 4 — легаси стейт-менеджер
Заголовок раздела «📦 Vuex 4 — легаси стейт-менеджер»Vuex — это официальный стейт-менеджер для Vue, который был стандартом до появления Pinia. В 2024 году для новых проектов рекомендуется Pinia, но Vuex всё ещё встречается в тысячах существующих проектов. Важно уметь читать, поддерживать и мигрировать код на Vuex 🔄
Установка
Заголовок раздела «Установка»npm install vuex@next # Vuex 4 для Vue 3import { createStore } from 'vuex'
const store = createStore({ state() { return { count: 0, user: null, } },})
export default storeimport { createApp } from 'vue'import App from './App.vue'import store from './store'
createApp(App).use(store).mount('#app')Архитектура Vuex
Заголовок раздела «Архитектура Vuex» Component │ │ dispatch(action) ▼ Actions ← async операции (API запросы) │ │ commit(mutation) ▼ Mutations ← ТОЛЬКО синхронные изменения! │ │ изменяет ▼ State ← единственный источник истины │ │ читается через ▼ Getters ← вычисляемые данные │ ▼ Component ← реактивно обновляетсяState — состояние
Заголовок раздела «State — состояние»const store = createStore({ state() { return { count: 0, users: [] as User[], currentUser: null as User | null, loading: false, error: null as string | null, } },})<script setup lang="ts">import { useStore } from 'vuex'import { computed } from 'vue'
const store = useStore()
// Читаем state через computedconst count = computed(() => store.state.count)const users = computed(() => store.state.users)
// Или через mapStateimport { mapState } from 'vuex'// В Options API: computed: { ...mapState(['count', 'users']) }</script>Getters — вычисляемые свойства
Заголовок раздела «Getters — вычисляемые свойства»const store = createStore({ state() { return { todos: [ { id: 1, text: 'Изучить Vuex', done: true }, { id: 2, text: 'Изучить Pinia', done: false }, ] } },
getters: { // Простой getter doneTodos(state) { return state.todos.filter(t => t.done) },
// Getter использует другой getter doneTodosCount(state, getters) { return getters.doneTodos.length },
// Getter возвращает функцию (для параметров) getTodoById(state) { return (id: number) => state.todos.find(t => t.id === id) }, },})<script setup lang="ts">const store = useStore()
const doneTodos = computed(() => store.getters.doneTodos)const count = computed(() => store.getters.doneTodosCount)
// С аргументомconst todo = computed(() => store.getters.getTodoById(1))</script>Mutations — синхронные изменения состояния
Заголовок раздела «Mutations — синхронные изменения состояния»Правило Vuex: нельзя менять state напрямую! Только через мутации:
const store = createStore({ state: () => ({ count: 0, user: null as User | null, }),
mutations: { // Простая мутация INCREMENT(state) { state.count++ },
DECREMENT(state) { state.count = Math.max(0, state.count - 1) },
// Мутация с payload SET_COUNT(state, payload: number) { state.count = payload },
// Мутация с объектным payload SET_USER(state, payload: { user: User | null }) { state.user = payload.user },
// Соглашение: UPPER_SNAKE_CASE для мутаций RESET_STATE(state) { state.count = 0 state.user = null }, },})<script setup>const store = useStore()
// Вызов мутацииfunction increment() { store.commit('INCREMENT')}
function setCount(value) { store.commit('SET_COUNT', value) // Или объектный стиль: store.commit({ type: 'SET_COUNT', payload: value })}</script>Actions — асинхронные операции
Заголовок раздела «Actions — асинхронные операции»const store = createStore({ state: () => ({ users: [] as User[], loading: false, error: null as string | null, }),
mutations: { SET_LOADING(state, loading: boolean) { state.loading = loading }, SET_USERS(state, users: User[]) { state.users = users }, SET_ERROR(state, error: string | null) { state.error = error }, },
actions: { // Actions получают context (commit, state, getters, dispatch, rootState) async fetchUsers({ commit }) { commit('SET_LOADING', true) commit('SET_ERROR', null)
try { const response = await fetch('/api/users') const users = await response.json() commit('SET_USERS', users) } catch (e) { commit('SET_ERROR', e instanceof Error ? e.message : 'Ошибка') } finally { commit('SET_LOADING', false) } },
// Action может диспатчить другие actions async loginAndFetch({ dispatch }, credentials: Credentials) { await dispatch('login', credentials) // Другой action await dispatch('fetchUsers') // Ещё один },
// Action возвращает Promise! async createUser({ commit }, userData: Partial<User>) { const response = await fetch('/api/users', { method: 'POST', body: JSON.stringify(userData), }) const user = await response.json() commit('ADD_USER', user) return user // Возвращаем результат }, },})<script setup>const store = useStore()
async function handleCreateUser() { try { // dispatch возвращает Promise! const newUser = await store.dispatch('createUser', { name: 'Яша', }) console.log('Создан:', newUser) } catch (e) { console.error(e) }}</script>Modules — разбивка на модули
Заголовок раздела «Modules — разбивка на модули»const userModule = { namespaced: true, // Пространство имён!
state: () => ({ currentUser: null as User | null, token: '', }),
getters: { isLoggedIn: (state) => !!state.currentUser, displayName: (state) => state.currentUser?.name ?? 'Гость', },
mutations: { SET_USER(state, user: User | null) { state.currentUser = user }, SET_TOKEN(state, token: string) { state.token = token }, },
actions: { async login({ commit }, { email, password }) { const data = await api.login(email, password) commit('SET_USER', data.user) commit('SET_TOKEN', data.token) },
logout({ commit }) { commit('SET_USER', null) commit('SET_TOKEN', '') }, },}
// store/modules/cart.tsconst cartModule = { namespaced: true, state: () => ({ items: [] }), // ...}
// store/index.ts — объединяем модулиconst store = createStore({ modules: { user: userModule, // Доступ: store.state.user.* cart: cartModule, // Доступ: store.state.cart.* }})<script setup>const store = useStore()
// Обращение к namespaced модулюconst isLoggedIn = computed(() => store.getters['user/isLoggedIn'])
function login(credentials) { store.dispatch('user/login', credentials)}
function logout() { store.dispatch('user/logout')}</script>mapState / mapGetters / mapMutations / mapActions
Заголовок раздела «mapState / mapGetters / mapMutations / mapActions»Хелперы для Options API:
<script>import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default { computed: { // Без пространства имён ...mapState(['count', 'users']), ...mapGetters(['doneTodos', 'doneTodosCount']),
// С пространством имён ...mapState('user', ['currentUser', 'token']), ...mapGetters('user', ['isLoggedIn', 'displayName']),
// Переименование ...mapState({ myCount: state => state.count, myUsers: 'users', // Сокращение }), },
methods: { ...mapMutations(['INCREMENT', 'DECREMENT']), ...mapMutations('cart', ['ADD_ITEM', 'REMOVE_ITEM']), ...mapActions(['fetchUsers']), ...mapActions('user', { doLogin: 'login', doLogout: 'logout' }), },}</script>useStore в Composition API
Заголовок раздела «useStore в Composition API»// store/composables.ts — типизированный useStoreimport { InjectionKey } from 'vue'import { useStore as baseUseStore, Store } from 'vuex'
interface State { count: number user: User | null}
export const key: InjectionKey<Store<State>> = Symbol()
export function useStore() { return baseUseStore<State>(key)}// main.ts — передаём ключapp.use(store, key)<!-- В компоненте --><script setup lang="ts">import { useStore } from '@/store/composables'
const store = useStore()// Теперь есть типизация! store.state.count — number</script>Когда мигрировать на Pinia
Заголовок раздела «Когда мигрировать на Pinia»Оставь Vuex если:✅ Проект работает, нет времени на рефакторинг✅ Команда привыкла к Vuex✅ Нет критических проблем с типизацией
Мигрируй на Pinia если:✅ Начинаешь новый проект✅ TypeScript становится болью в Vuex✅ Хочешь Composition API стиль✅ Устал от мутаций и сложных модулейПростой план миграции
Заголовок раздела «Простой план миграции»// 1. Установи Pinia рядом с Vuexnpm install pinia
// 2. Добавь Pinia в main.tsapp.use(createPinia())app.use(store) // Vuex остаётся!
// 3. Мигрируй по одному модулю// Vuex user модуль → Pinia useUserStore
// 4. В компонентах замени store.state.user → useUserStore()
// 5. Когда все компоненты перешли — удали Vuex модуль
// 6. Когда все модули перенесены — удали Vuex совсем