Композиция stores — ключевая идея современной архитектуры состояния во Vue.js, особенно в экосистеме Vue 3 и Pinia. Вместо монолитных глобальных хранилищ используется разбиение логики на независимые, переиспользуемые и легко тестируемые единицы. Каждый store инкапсулирует конкретную предметную область, а композиция позволяет связывать их между собой без жёстких зависимостей.
Такой подход основан на принципах Composition API: состояние, вычисления и побочные эффекты группируются по смыслу, а не по типу.
Pinia — официальный менеджер состояния для Vue 3, полностью совместимый с Composition API. Store в Pinia по своей природе является функцией, что делает композицию естественной.
Пример базового store:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const isAuthenticated = computed(() => !!user.value)
function setUser(data) {
user.value = data
}
return {
user,
isAuthenticated,
setUser
}
})
Store здесь — это не объект, а композиционная функция, возвращающая состояние и методы.
Композиция stores означает, что один store может использовать другой напрямую, вызывая его как обычную функцию. Это не создаёт глобальной зависимости, так как Pinia управляет единственным экземпляром.
import { defineStore } from 'pinia'
import { computed } from 'vue'
import { useUserStore } from './user'
export const usePermissionsStore = defineStore('permissions', () => {
const userStore = useUserStore()
const canEdit = computed(() => {
return userStore.user?.role === 'admin'
})
return {
canEdit
}
})
Ключевые особенности:
При композиции stores важно не допускать ситуаций, когда два store напрямую зависят друг от друга. Это приводит к трудноотлавливаемым ошибкам и нарушению архитектуры.
Неправильный пример:
useUserStore использует
usePermissionsStoreusePermissionsStore использует
useUserStoreКорректные подходы:
Композиция stores не означает, что вся логика должна находиться в них. Чистая бизнес-логика, не зависящая от глобального состояния, выносится в composables.
// useDateFormatter.js
export function useDateFormatter() {
function format(date) {
return new Intl.DateTimeFormat('ru-RU').format(date)
}
return { format }
}
Использование в store:
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useDateFormatter } from '@/composables/useDateFormatter'
export const useLogStore = defineStore('log', () => {
const logs = ref([])
const { format } = useDateFormatter()
function addLog(message) {
logs.value.push({
message,
date: format(new Date())
})
}
return { logs, addLog }
})
Такой подход делает store компактными и сфокусированными.
Асинхронные действия — важная часть state-менеджмента. В композиционной архитектуре они легко комбинируются между stores.
export const usePostsStore = defineStore('posts', () => {
const posts = ref([])
const loading = ref(false)
async function fetchPosts() {
loading.value = true
posts.value = await api.getPosts()
loading.value = false
}
return { posts, loading, fetchPosts }
})
Использование в другом store:
export const useDashboardStore = defineStore('dashboard', () => {
const postsStore = usePostsStore()
const postCount = computed(() => postsStore.posts.length)
return { postCount }
})
Асинхронность не нарушает композицию, так как реактивность основана
на ссылках (ref).
Композиция stores позволяет избегать копирования состояния. Один store владеет данными, другие — только читают или реагируют.
Принципы:
watch для синхронизации между storesИногда store должен быть более универсальным. В таких случаях используется параметризация.
export const useCounterStore = defineStore('counter', () => {
function createCounter(initial = 0) {
const count = ref(initial)
const inc = () => count.value++
return { count, inc }
}
return { createCounter }
})
Этот приём позволяет использовать store как фабрику реактивных сущностей.
Композиция повышает тестируемость за счёт:
Store можно тестировать изолированно, подменяя зависимые composables или другие stores.
const userStore = useUserStore()
userStore.setUser({ role: 'admin' })
const permissionsStore = usePermissionsStore()
expect(permissionsStore.canEdit).toBe(true)
При росте приложения количество stores увеличивается, но композиционный подход предотвращает хаос.
Рекомендуемая структура:
stores/
├─ user/
│ ├─ user.store.js
│ └─ permissions.store.js
├─ posts/
│ └─ posts.store.js
└─ ui/
└─ modal.store.js
Каждый модуль может содержать несколько связанных stores, использующих друг друга через композицию, но не пересекающихся с другими доменами.
Store в Pinia — это частный случай composable. Он подчиняется тем же правилам:
Это стирает границу между локальным и глобальным состоянием, делая архитектуру однородной и предсказуемой.