Vue.js course Β· Module 11: Pinia
Store Composition
In this lesson3
At NOVA LAB, different station systems must work together - the oxygen system communicates with the alarm system, and the navigation system with the communication system. Similarly in Pinia - stores can import and use other stores.
Store Using Another Store
1// stores/auth.js
2import { defineStore } from 'pinia'
3import { ref, computed } from 'vue'
4
5export const useAuthStore = defineStore('auth', () => {
6 const user = ref(null)
7 const token = ref(null)
8
9 const isAuthenticated = computed(() => !!token.value)
10 const userName = computed(() => user.value?.name || 'Guest')
11
12 async function login(credentials) {
13 const res = await fetch('/api/login', {
14 method: 'POST',
15 headers: { 'Content-Type': 'application/json' },
16 body: JSON.stringify(credentials)
17 })
18 const data = await res.json()
19 token.value = data.token
20 user.value = data.user
21 }
22
23 function logout() {
24 user.value = null
25 token.value = null
26 }
27
28 return { user, token, isAuthenticated, userName, login, logout }
29})1// stores/missions.js - uses auth store
2import { defineStore } from 'pinia'
3import { ref } from 'vue'
4import { useAuthStore } from './auth'
5
6export const useMissionsStore = defineStore('missions', () => {
7 const missions = ref([])
8
9 async function fetchMissions() {
10 // Use auth store to get the token
11 const authStore = useAuthStore()
12
13 if (!authStore.isAuthenticated) {
14 throw new Error('Must be logged in')
15 }
16
17 const res = await fetch('/api/missions', {
18 headers: {
19 'Authorization': `Bearer ${authStore.token}`
20 }
21 })
22
23 missions.value = await res.json()
24 }
25
26 return { missions, fetchMissions }
27})Mutual Store Dependencies
Stores can use each other, but call useStore() inside actions (not at the top of the setup function) to avoid circular dependency issues:
1// stores/notifications.js
2import { defineStore } from 'pinia'
3import { ref } from 'vue'
4
5export const useNotificationStore = defineStore('notifications', () => {
6 const notifications = ref([])
7
8 function add(message, type = 'info') {
9 notifications.value.push({
10 id: Date.now(),
11 message,
12 type,
13 timestamp: new Date()
14 })
15 }
16
17 function remove(id) {
18 notifications.value = notifications.value.filter(n => n.id !== id)
19 }
20
21 return { notifications, add, remove }
22})
23
24// stores/resources.js - uses notifications
25import { useNotificationStore } from './notifications'
26
27export const useResourceStore = defineStore('resources', () => {
28 const oxygen = ref(100)
29
30 function consumeOxygen(amount) {
31 oxygen.value -= amount
32
33 if (oxygen.value < 20) {
34 // Call useStore INSIDE action
35 const notifStore = useNotificationStore()
36 notifStore.add('Oxygen level critical!', 'danger')
37 }
38 }
39
40 return { oxygen, consumeOxygen }
41})Multi-Store Architecture
It's recommended to create many small, specialized stores instead of one large one:
1stores/
2βββ auth.js # User authorization
3βββ resources.js # Station resources (oxygen, water, energy)
4βββ crew.js # Crew data
5βββ missions.js # Missions and tasks
6βββ notifications.js # Notification system
7βββ settings.js # Application settingsEach store is responsible for one "module" of the application - just as each NOVA LAB station module has its own function.
Code for this lesson: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// Store composition - AuthStore
5const user = ref({ name: 'Commander Nova', role: 'admin' })
6const token = ref('jwt-token-xyz')
7const isAuthenticated = computed(() => !!token.value)
8
9// Store composition - ResourceStore (uses AuthStore)
10const resources = ref([
11 { id: 1, name: 'Oxygen', level: 92, unit: '%' },
12 { id: 2, name: 'Water', level: 78, unit: '%' },
13 { id: 3, name: 'Energy', level: 65, unit: '%' }
14])
15
16// Store composition - NotificationStore
17const notifications = ref([])
18function addNotification(message, type = 'info') {
19 const id = Date.now()
20 notifications.value.push({ id, message, type, time: new Date().toLocaleTimeString() })
21 setTimeout(() => {
22 notifications.value = notifications.value.filter(n => n.id !== id)
23 }, 4000)
24}
25
26// ResourceStore actions use NotificationStore
27function consumeResource(id, amount) {
28 const resource = resources.value.find(r => r.id === id)
29 if (!resource) return
30
31 resource.level = Math.max(0, resource.level - amount)
32
33 if (resource.level < 20) {
34 // Cross-store: resource -> notification
35 addNotification(`${resource.name} critically low: ${resource.level}${resource.unit}!`, 'danger')
36 } else if (resource.level < 50) {
37 addNotification(`${resource.name} below 50%: ${resource.level}${resource.unit}`, 'warning')
38 }
39}
40
41function refillResource(id) {
42 const resource = resources.value.find(r => r.id === id)
43 if (resource) {
44 resource.level = 100
45 addNotification(`${resource.name} refilled to 100%`, 'success')
46 }
47}
48
49// Cross-store getter
50const criticalResources = computed(() =>
51 resources.value.filter(r => r.level < 30)
52)
53</script>
54
55<template>
56 <div class="nova-lab">
57 <h1>NOVA LAB - Store Composition</h1>
58
59 <div class="user-bar" v-if="isAuthenticated">
60 Logged in: {{ user.name }} ({{ user.role }})
61 </div>
62
63 <div class="resources-grid">
64 <div v-for="r in resources" :key="r.id"
65 class="resource-card" :class="{ critical: r.level < 20, warning: r.level < 50 }">
66 <h3>{{ r.name }}</h3>
67 <div class="level-bar">
68 <div class="level-fill" :style="{ width: r.level + '%' }"></div>
69 </div>
70 <div class="level-text">{{ r.level }}{{ r.unit }}</div>
71 <div class="r-actions">
72 <button @click="consumeResource(r.id, 15)" class="btn">-15</button>
73 <button @click="refillResource(r.id)" class="btn btn-green">Refill</button>
74 </div>
75 </div>
76 </div>
77
78 <div v-if="criticalResources.length" class="critical-alert">
79 CRITICAL: {{ criticalResources.map(r => r.name).join(', ') }}
80 </div>
81
82 <div class="notifications">
83 <div v-for="n in notifications" :key="n.id" class="notif" :class="n.type">
84 {{ n.time }} - {{ n.message }}
85 </div>
86 </div>
87 </div>
88</template>
89
90<style scoped>
91.nova-lab {
92 background: #0a0e27;
93 color: #00ff88;
94 min-height: 100vh;
95 padding: 20px;
96 font-family: monospace;
97}
98h1 { color: #00ff88; margin-bottom: 15px; text-align: center; }
99.user-bar {
100 text-align: center;
101 padding: 8px;
102 background: rgba(0, 180, 216, 0.1);
103 border: 1px solid #00b4d8;
104 margin-bottom: 15px;
105 color: #00b4d8;
106}
107.resources-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 15px; }
108.resource-card {
109 background: rgba(0, 255, 136, 0.05);
110 border: 2px solid #00ff88;
111 padding: 15px;
112 text-align: center;
113}
114.resource-card.warning { border-color: #ff9900; }
115.resource-card.critical { border-color: #ff4444; background: rgba(255, 68, 68, 0.1); }
116h3 { color: #00b4d8; margin-bottom: 8px; }
117.level-bar { height: 12px; background: #1a1e3f; border: 1px solid #00b4d8; margin: 8px 0; }
118.level-fill { height: 100%; background: #00ff88; transition: width 0.3s; }
119.resource-card.warning .level-fill { background: #ff9900; }
120.resource-card.critical .level-fill { background: #ff4444; }
121.level-text { font-size: 1.2em; font-weight: bold; margin-bottom: 8px; }
122.r-actions { display: flex; gap: 5px; justify-content: center; }
123.btn {
124 background: #00b4d8;
125 color: #0a0e27;
126 border: none;
127 padding: 5px 12px;
128 cursor: pointer;
129 font-family: monospace;
130}
131.btn-green { background: #00ff88; }
132.critical-alert {
133 padding: 10px;
134 background: rgba(255, 68, 68, 0.2);
135 border: 2px solid #ff4444;
136 color: #ff4444;
137 text-align: center;
138 margin-bottom: 15px;
139 font-weight: bold;
140}
141.notifications { max-width: 500px; margin: 0 auto; }
142.notif {
143 padding: 8px;
144 margin: 4px 0;
145 border: 1px solid;
146 font-size: 0.9em;
147}
148.notif.info { border-color: #00b4d8; color: #00b4d8; }
149.notif.warning { border-color: #ff9900; color: #ff9900; }
150.notif.danger { border-color: #ff4444; color: #ff4444; }
151.notif.success { border-color: #00ff88; color: #00ff88; }
152</style>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. How can one Pinia store use another store?
2. Where should useStore() be called when using stores in other stores?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create stores that communicate with each other
- Vertical ordering
Arrange the layers of multi-store architecture
- Click in order
Arrange the elements to call an action from another store
- Code editor
Create a dashboard using multiple stores