Олег Марков
Vuex - полное руководство по управлению состоянием во Vue приложениях
Введение
Vuex — это официальная библиотека для управления состоянием в приложениях на Vue 2 и Vue 3 (через совместимый пакет). Она помогает организовать данные так, чтобы разные компоненты могли предсказуемо их читать и изменять.
Смотрите, идея простая. Когда у вас один‑два компонента, вы спокойно передаёте данные через props и генерируете события вверх. Но как только приложение растёт, состояние начинает "расползаться" по разным частям, появляются дублирования и трудно отследить, кто и когда изменяет данные. Vuex решает именно эту проблему.
Ключевая мысль: Vuex вводит единый центр хранения данных — стор (store) — и строгие правила, как это состояние можно обновлять. Это делает поведение приложения более предсказуемым, а отладку — проще.
В этой статье вы увидите:
- как устроен стор Vuex;
- что такое state, getters, mutations, actions;
- как подключать Vuex к Vue приложению;
- как работать с модулями;
- как организовывать код в реальных проектах.
Я буду показывать код с комментариями, чтобы вы могли шаг за шагом проследить, как всё работает.
Хотите разобраться глубже? Пройдите Курс Vue 3 и Pinia с нуля — практика, наставники, реальные проекты.
Курс по теме
Курс Vue 3 и Pinia с нуля
40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней
Основные концепции Vuex
Одно хранилище для всего приложения
Vuex предлагает использовать один общий стор для всего приложения. Это не означает, что весь код будет в одном файле — наоборот, мы можем разбивать стор на модули. Но с точки зрения архитектуры состояние централизовано.
Схематично взаимодействие выглядит так:
- Компонент не меняет состояние напрямую.
- Компонент вызывает action (действие).
- Action, при необходимости, выполняет асинхронный код.
- Action вызывает mutation (мутацию).
- Мутация синхронно изменяет state.
- Компоненты "подписаны" на state и автоматически обновляются.
Такой однонаправленный поток данных сильно упрощает понимание, что и где происходит.
Из чего состоит стор Vuex
Стор Vuex — это объект со следующими основными частями:
- state — данные;
- getters — вычисляемые свойства над state;
- mutations — синхронные изменения state;
- actions — бизнес‑логика и асинхронный код;
- modules — разбиение стора на части.
Давайте разберём каждую часть по отдельности.
State — централизованное состояние
Что такое state
State — это источник истинного состояния вашего приложения. Можно думать о нём как об одном большом объекте, содержащем все важные данные, которыми должны делиться компоненты.
Пример простого стора:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex) // Подключаем плагин Vuex к Vue 2
export default new Vuex.Store({
// Здесь мы описываем глобальное состояние приложения
state: {
// Счётчик
count: 0,
// Пользователь
user: {
id: null,
name: null,
isAdmin: false
},
// Список задач
todos: [] // Изначально список пустой
}
})
State — это "источник правды". Компоненты не должны хранить свои копии этих данных, если они уже есть в сторе. Вместо этого они берут данные напрямую из store.
Как читать state в компонентах
Есть несколько способов получить доступ к state из компонента.
Через this.$store.state
Самый прямой путь:
// Пример компонента Counter.vue
export default {
computed: {
// Здесь мы объявляем вычисляемое свойство count
// Оно будет брать данные из Vuex стора
count() {
return this.$store.state.count
}
}
}
Этот способ работает, но в больших компонентах код начинает "засоряться". Поэтому Vuex предлагает хелперы.
Через mapState
Хелпер mapState позволяет подключать свойства state как локальные вычисляемые свойства.
// Counter.vue
import { mapState } from 'vuex'
export default {
computed: {
// Здесь мы разворачиваем объект из mapState в локальные computed
...mapState({
// Левое имя - имя локального computed
// Правое выражение - путь к данным в state
count: state => state.count,
userName: state => state.user.name
})
}
}
Можно использовать более короткую запись, если имя совпадает:
import { mapState } from 'vuex'
export default {
computed: {
// В этом случае Vuex будет искать state.count и state.todos
...mapState(['count', 'todos'])
}
}
Такой подход делает связи с состоянием явными и удобочитаемыми.
Getters — вычисляемые представления данных
Зачем нужны getters
В компонентах часто нужны не "сырые" данные, а их производные:
- отфильтрованный список;
- количество элементов по какому‑то условию;
- комбинированные данные из разных частей state.
Конечно, вы можете считать это прямо в компонентах, но тогда логика будет дублироваться. Getters решают это: они похожи на computed‑свойства для store.
Пример использования getters
// store/index.js
export default new Vuex.Store({
state: {
todos: [
{ id: 1, text: 'Купить молоко', done: false },
{ id: 2, text: 'Написать статью', done: true }
]
},
getters: {
// Возвращаем только выполненные задачи
doneTodos(state) {
// Здесь мы фильтруем массив по полю done
return state.todos.filter(todo => todo.done)
},
// Возвращаем количество выполненных задач
doneTodosCount(state, getters) {
// Мы можем использовать другие геттеры внутри геттера
return getters.doneTodos.length
},
// Геттер, который возвращает функцию - для параметров
getTodoById: (state) => (id) => {
// Здесь мы ищем задачу по идентификатору
return state.todos.find(todo => todo.id === id)
}
}
})
Getters кэшируются так же, как computed. Пока зависимое состояние не меняется, геттер не будет пересчитываться.
Как использовать getters в компонентах
Точно так же, как state, через this.$store или mapGetters.
import { mapGetters } from 'vuex'
export default {
computed: {
// Подключаем геттеры в качестве computed-свойств компонента
...mapGetters(['doneTodos', 'doneTodosCount']),
// Можем переименовывать локальные имена
...mapGetters({
completed: 'doneTodos'
})
},
methods: {
showTodo(id) {
// Когда геттер возвращает функцию, мы вызываем его как обычную функцию
const todo = this.$store.getters.getTodoById(id)
console.log(todo)
}
}
}
Getters помогают вынести повторяющуюся логику выборки и обработки данных из компонентов в одно место.
Mutations — единственный способ изменить state
Основной принцип
Очень важное правило Vuex:
- НЕЛЬЗЯ изменять
stateнапрямую из компонента. - МОЖНО изменять
stateтолько черезmutations.
Почему так? Мутации:
- всегда синхронные;
- всегда явные;
- легко отслеживаются в Vue Devtools.
Это позволяет понять, какие именно изменения были сделаны и в какой последовательности.
Пример мутаций
export default new Vuex.Store({
state: {
count: 0,
todos: []
},
mutations: {
// Простая мутация - увеличение счётчика
increment(state) {
// Здесь мы напрямую изменяем state
state.count++
},
// Мутация с полезной нагрузкой (payload)
setCount(state, newCount) {
// Мы явно задаём новое значение счётчика
state.count = newCount
},
// Мутация с объектом payload
addTodo(state, payload) {
// Ожидаем, что payload - это объект задачи
state.todos.push(payload)
}
}
})
Обратите внимание: мутации всегда получают state первым аргументом, а вторым — полезную нагрузку (payload), если она нужна.
Как вызывать mutations из компонентов
Мутации вызываются через метод commit:
export default {
methods: {
increment() {
// Вызываем мутацию increment
this.$store.commit('increment')
},
setSpecificCount() {
// Передаём конкретное значение счётчика
this.$store.commit('setCount', 10)
},
addNewTodo() {
// Передаём объект задачи
this.$store.commit('addTodo', {
id: Date.now(),
text: 'Новая задача',
done: false
})
}
}
}
Для удобства есть хелпер mapMutations:
import { mapMutations } from 'vuex'
export default {
methods: {
// Здесь мы подключаем мутации как методы компонента
...mapMutations(['increment', 'addTodo']),
// Можно переименовать локальный метод
...mapMutations({
set: 'setCount'
})
}
}
Смотрите, я показываю простой пример: теперь вы можете в шаблоне писать @click="increment" и быть уверенными, что это приведёт к предсказуемому изменению state.
Actions — бизнес‑логика и асинхронность
Зачем нужны actions
Если мутации должны быть синхронными, то как быть с асинхронными операциями, например HTTP‑запросами? Для этого нужны actions.
Actions:
- могут выполнять асинхронный код;
- могут вызывать несколько мутаций;
- могут вызывать другие actions;
- инкапсулируют бизнес‑логику.
Важно: actions не изменяют state напрямую. Они вызывают мутации через commit.
Пример actions
// store/index.js
export default new Vuex.Store({
state: {
todos: [],
isLoading: false,
error: null
},
mutations: {
setLoading(state, value) {
// Устанавливаем флаг загрузки
state.isLoading = value
},
setError(state, error) {
// Сохраняем текст ошибки
state.error = error
},
setTodos(state, todos) {
// Заменяем список задач новыми данными
state.todos = todos
}
},
actions: {
// Асинхронное действие для загрузки задач
async fetchTodos({ commit }) {
// Перед началом запроса включаем индикатор загрузки
commit('setLoading', true)
commit('setError', null)
try {
// Здесь мы выполняем асинхронный HTTP-запрос
const response = await fetch('/api/todos')
// Проверяем успешность ответа
if (!response.ok) {
throw new Error('Ошибка при загрузке задач')
}
const data = await response.json()
// Сохраняем полученные задачи в state
commit('setTodos', data)
} catch (error) {
// В случае ошибки записываем её текст
commit('setError', error.message)
} finally {
// В любом случае отключаем индикатор загрузки
commit('setLoading', false)
}
}
}
})
В этом примере вы видите типичный паттерн: action управляет процессом, а мутации изменяют состояние шаг за шагом.
Как вызывать actions из компонентов
Actions вызываются через dispatch:
export default {
methods: {
loadTodos() {
// Запускаем асинхронное действие
this.$store.dispatch('fetchTodos')
}
},
mounted() {
// Например, загружаем данные при монтировании компонента
this.loadTodos()
}
}
Конечно, есть и хелпер mapActions:
import { mapActions } from 'vuex'
export default {
methods: {
// Подключаем действия как методы компонента
...mapActions(['fetchTodos']),
// Переименование
...mapActions({
loadTodos: 'fetchTodos'
})
}
}
Actions — правильное место для:
- HTTP‑запросов;
- вызова нескольких мутаций подряд;
- выполнения сложной логики, зависящей от внешних данных.
Подключение Vuex к приложению Vue
Пример для Vue 2
Для Vue 2 процесс достаточно прямой.
// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store' // Импортируем стор
new Vue({
store, // Подключаем стор ко всему приложению
render: h => h(App) // Рендерим корневой компонент
}).$mount('#app')
После этого во всех компонентах будет доступен this.$store.
Пример для Vue 3
Vue 3 использует немного другой синтаксис, но сама идея та же.
// store/index.js
import { createStore } from 'vuex'
// Создаём стор с помощью функции createStore
export const store = createStore({
state: {
count: 0
},
mutations: {
increment(state) {
// Увеличиваем счётчик
state.count++
}
}
})
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { store } from './store' // Импортируем созданный стор
const app = createApp(App)
// Подключаем Vuex стор к приложению
app.use(store)
// Монтируем приложение в DOM
app.mount('#app')
Теперь во всех компонентах Vue 3 также будет доступен this.$store (в опциональном API) или useStore (в Composition API).
Vuex и Composition API
Доступ к стору через useStore
В приложениях на Vue 3 с Composition API удобнее использовать хук useStore.
// Counter.vue
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
setup() {
// Получаем экземпляр стора
const store = useStore()
// Создаём вычисляемое свойство, связанное со state.count
const count = computed(() => store.state.count)
// Создаём метод для увеличения счётчика через мутацию
const increment = () => {
store.commit('increment')
}
// Возвращаем свойства и методы в шаблон
return {
count,
increment
}
}
}
Здесь вы видите, что логика становится чуть более "функциональной": мы явно берём store, создаём обёртки поверх state и mutations и возвращаем их.
Модули Vuex — структурирование большого стора
Зачем нужны модули
В реальном приложении один файл со всем state быстро становится громоздким. Чтобы избежать "гигантского" стора, Vuex поддерживает модули.
Модуль — это мини‑стор со своим state, mutations, actions и getters. Все модули соединяются в один общий стор.
Пример организации модулей
Допустим, у нас есть две области данных:
- аутентификация пользователя;
- список задач.
Смотрите, я покажу вам, как это разбить на модули.
// store/modules/auth.js
export const auth = {
// Пространство имён по умолчанию выключено, рассмотрим его позже
state: () => ({
user: null,
token: null
}),
mutations: {
setUser(state, user) {
state.user = user
},
setToken(state, token) {
state.token = token
},
logout(state) {
state.user = null
state.token = null
}
},
actions: {
async login({ commit }, credentials) {
// Здесь должен быть реальный HTTP-запрос к API авторизации
// Для примера используем фейковые данные
const fakeUser = { id: 1, name: 'Test User' }
const fakeToken = 'abc123'
// Сохраняем данные пользователя и токен
commit('setUser', fakeUser)
commit('setToken', fakeToken)
}
},
getters: {
isAuthenticated(state) {
// Пользователь считается авторизованным, если есть токен
return !!state.token
}
}
}
// store/modules/todos.js
export const todos = {
state: () => ({
items: []
}),
mutations: {
setTodos(state, todos) {
state.items = todos
},
addTodo(state, todo) {
state.items.push(todo)
}
},
actions: {
async fetchTodos({ commit }) {
// Здесь должен быть реальный HTTP-запрос
const fakeTodos = [
{ id: 1, text: 'Пример задачи', done: false }
]
// Сохраняем фейковые данные в state
commit('setTodos', fakeTodos)
}
},
getters: {
completedTodos(state) {
// Возвращаем только выполненные задачи
return state.items.filter(t => t.done)
}
}
}
Теперь подключим модули к общему стору.
// store/index.js
import { createStore } from 'vuex'
import { auth } from './modules/auth'
import { todos } from './modules/todos'
// Создаём главный стор и регистрируем модули
export const store = createStore({
modules: {
auth, // Модуль авторизации
todos // Модуль задач
}
})
Доступ к модульному state и геттерам
Теперь state и getters разделены по модулям:
// Пример доступа в компоненте
export default {
computed: {
// Доступ к модульному state
user() {
return this.$store.state.auth.user
},
todos() {
return this.$store.state.todos.items
},
// Доступ к модульному геттеру
isAuthenticated() {
return this.$store.getters.isAuthenticated
}
}
}
А вот с actions и mutations есть нюанс: имена по умолчанию попадают в глобальное пространство имён. Повторяющиеся названия будут конфликтовать. Для решения этого Vuex предлагает namespaced‑модули.
Модули с пространством имён (namespaced)
Зачем нужно namespaced
Если вы используете много модулей, названия мутаций и действий легко могут пересекаться: setLoading, setError, fetchData и так далее. Чтобы избежать конфликтов и сделать код более читаемым, можно включить namespaced: true.
// store/modules/auth.js
export const auth = {
namespaced: true, // Включаем пространство имён для модуля
state: () => ({
user: null,
token: null
}),
mutations: {
setUser(state, user) {
state.user = user
}
},
actions: {
async login({ commit }, credentials) {
// Здесь могла бы быть реальная авторизация
const fakeUser = { id: 1, name: 'Auth User' }
commit('setUser', fakeUser)
}
},
getters: {
isAuthenticated(state) {
return !!state.token
}
}
}
Теперь имена "полных" действий и мутаций будут выглядеть как auth/login, auth/setUser.
Вызов действий и мутаций у namespaced модулей
export default {
methods: {
// Вызов namespaced-действия
login() {
// Вызываем действие login из модуля auth
this.$store.dispatch('auth/login', { email: 'a@b.c', password: '123' })
}
},
computed: {
isAuthenticated() {
// Доступ к геттеру auth/isAuthenticated
return this.$store.getters['auth/isAuthenticated']
}
}
}
mapActions и mapGetters с пространствами имён
Хелперы поддерживают указание имени модуля.
import { mapActions, mapGetters } from 'vuex'
export default {
computed: {
// Подключаем геттеры из модуля auth
...mapGetters('auth', ['isAuthenticated'])
},
methods: {
// Подключаем действия из модуля auth
...mapActions('auth', ['login'])
}
}
Здесь я показываю удобный приём: модульное имя передаётся первым аргументом, а дальше — список геттеров или действий.
Паттерны и лучшие практики при работе с Vuex
Когда стоит использовать Vuex
Vuex хорошо подходит, когда:
- у вас среднее или большое приложение;
- одни и те же данные нужны в разных, не связанных напрямую компонентах;
- важна прозрачная отладка и предсказуемость;
- вы хотите явно отделить бизнес‑логику от компонентов.
Не всегда нужно начинать с Vuex в самом начале. Но как только вы замечаете:
- что много данных приходится "проталкивать" через цепочку компонентов;
- что появляются "глобальные" события;
- что становится трудно понять, кто и когда изменяет данные,
— имеет смысл вынести общее состояние в Vuex.
Организация файловой структуры
Хорошей практикой является разделение по доменам:
store/modules/auth.jsstore/modules/todos.jsstore/modules/profile.js- и так далее.
В каждом модуле удобно группировать:
- state вверху;
- затем getters;
- затем mutations;
- затем actions.
Это помогает быстро ориентироваться в коде.
Где держать бизнес‑логику
Основной принцип: бизнес‑логика должна быть в actions, а не в компонентах.
Компонент:
- отвечает за отображение;
- вызывает actions и отображает результат.
Actions:
- описывают, что нужно сделать с данными;
- выполняют запросы к API;
- вызывают нужные мутации.
Такой подход делает компоненты более простыми, а логику — переиспользуемой.
Избегайте избыточного использования Vuex
Не нужно класть в Vuex всё подряд. Держите там только то, что:
- важно для нескольких частей приложения;
- должно переживать переходы между страницами (пока страницы в рамках одного SPA);
- нужно сохранять в истории (например, для "time travel" через Devtools).
Локальное состояние конкретного компонента (состояние модального окна, значение одного локального инпута и так далее) вполне можно хранить в самом компоненте.
Типичные ошибки при работе с Vuex
1. Изменение state напрямую
Проблема: изменение state не через мутации.
// Плохо - меняем state напрямую из компонента
this.$store.state.count++
Это ломает концепцию Vuex и делает отладку сложнее. Делать нужно только так:
// Хорошо - изменение происходит через мутацию
this.$store.commit('increment')
2. Асинхронный код внутри мутаций
Мутации должны быть синхронными. Вставка асинхронного кода приводит к непредсказуемому порядку изменений.
// Плохо
mutations: {
async incrementLater(state) {
// Асинхронный код внутри мутации - плохая идея
await new Promise(resolve => setTimeout(resolve, 1000))
state.count++
}
}
Нужно выносить асинхронность в actions:
actions: {
async incrementLater({ commit }) {
// Ждём одну секунду
await new Promise(resolve => setTimeout(resolve, 1000))
// Теперь вызываем синхронную мутацию
commit('increment')
}
}
3. Смешивание логики во всех слоях
Иногда в приложениях можно встретить:
- часть логики в компонентах;
- часть — в actions;
- часть — в мутациях.
Старайтесь придерживаться разделения:
- мутации — только изменение state;
- actions — бизнес‑логика и асинхронность;
- компоненты — только связка "показать данные" и "отреагировать на пользовательский ввод".
Заключение
Vuex вводит жёсткие, но понятные правила обращения с состоянием:
- одно централизованное хранилище;
- только мутации могут изменять state;
- actions управляют асинхронным кодом и бизнес‑логикой;
- getters предоставляют "представления" данных;
- модули помогают структурировать большой стор.
Такой подход особенно полезен, когда приложение растёт, команда увеличивается, а отладка и предсказуемость становятся критически важными. С помощью Vuex вы можете держать состояние под контролем, а компоненты — сделать более простыми и понятными.
Частозадаваемые технические вопросы по Vuex
Как типизировать Vuex стор в TypeScript
Создайте интерфейсы для state и используйте их при создании стора.
// Описываем тип состояния
interface RootState {
count: number
}
// Передаём тип в createStore
export const store = createStore<RootState>({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
}
})
В компонентах с Composition API используйте дженерик:
import { useStore } from 'vuex'
const store = useStore<RootState>()
Так вы получите типизированный доступ к state и геттерам.
Как сделать переиспользуемый модуль Vuex с динамическими именами
Используйте функцию‑фабрику и динамическую регистрацию модулей:
// Фабрика модуля
function createListModule() {
return {
namespaced: true,
state: () => ({ items: [] }),
mutations: {
setItems(state, items) {
state.items = items
}
}
}
}
// Динамическая регистрация
store.registerModule('products', createListModule())
store.registerModule('users', createListModule())
Теперь у вас два независимых модуля с одинаковой логикой.
Как сбросить состояние стора к начальному
Храните начальное состояние в функции и используйте мутацию для полного сброса.
const getDefaultState = () => ({
count: 0,
todos: []
})
const state = getDefaultState()
const mutations = {
resetState(state) {
Object.assign(state, getDefaultState())
}
}
Вызовите commit('resetState'), чтобы вернуть state в исходное состояние.
Как использовать Vuex в модульных тестах
Создавайте "тестовый" стор с нужной частью логики и подключайте его к компоненту.
import { createStore } from 'vuex'
import { shallowMount } from '@vue/test-utils'
import MyComponent from '@/components/MyComponent.vue'
const store = createStore({
state: { count: 0 },
mutations: {
increment(state) {
state.count++
}
}
})
const wrapper = shallowMount(MyComponent, {
global: {
plugins: [store] // Подключаем стор для теста
}
})
Так вы изолируете тест и сможете проверять взаимодейcтвие компонента со стором.
Как разделять Vuex код между несколькими проектами
Вынесите общие модули Vuex в отдельный npm‑пакет:
- Создайте отдельный репозиторий с модулями.
- Экспортируйте модули как функции‑фабрики (чтобы не было конфликтов state).
- Публикуйте пакет в приватный или публичный npm.
- Подключайте модули в проектах через
importиmodules.
Это помогает переиспользовать общую бизнес‑логику в нескольких приложениях.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев