Vue.js course Β· Module 11: Pinia

Store Composition

3 min read
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 settings

Each 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. 1. How can one Pinia store use another store?

  2. 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

Useful articles