Перейти к содержимому

17. Vuex 4 (legacy)

Vuex — это официальный стейт-менеджер для Vue, который был стандартом до появления Pinia. В 2024 году для новых проектов рекомендуется Pinia, но Vuex всё ещё встречается в тысячах существующих проектов. Важно уметь читать, поддерживать и мигрировать код на Vuex 🔄


Окно терминала
npm install vuex@next # Vuex 4 для Vue 3
store/index.ts
import { createStore } from 'vuex'
const store = createStore({
state() {
return {
count: 0,
user: null,
}
},
})
export default store
main.ts
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App).use(store).mount('#app')

Component
│ dispatch(action)
Actions ← async операции (API запросы)
│ commit(mutation)
Mutations ← ТОЛЬКО синхронные изменения!
│ изменяет
State ← единственный источник истины
│ читается через
Getters ← вычисляемые данные
Component ← реактивно обновляется

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 через computed
const count = computed(() => store.state.count)
const users = computed(() => store.state.users)
// Или через mapState
import { mapState } from 'vuex'
// В Options API: computed: { ...mapState(['count', 'users']) }
</script>

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>

Правило 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>

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>

store/modules/user.ts
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.ts
const 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>

Хелперы для 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>

// store/composables.ts — типизированный useStore
import { 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>

Оставь Vuex если:
✅ Проект работает, нет времени на рефакторинг
✅ Команда привыкла к Vuex
✅ Нет критических проблем с типизацией
Мигрируй на Pinia если:
✅ Начинаешь новый проект
✅ TypeScript становится болью в Vuex
✅ Хочешь Composition API стиль
✅ Устал от мутаций и сложных модулей
// 1. Установи Pinia рядом с Vuex
npm install pinia
// 2. Добавь Pinia в main.ts
app.use(createPinia())
app.use(store) // Vuex остаётся!
// 3. Мигрируй по одному модулю
// Vuex user модуль → Pinia useUserStore
// 4. В компонентах замени store.state.user → useUserStore()
// 5. Когда все компоненты перешли — удали Vuex модуль
// 6. Когда все модули перенесены — удали Vuex совсем