Vue.js course Β· Module 11: Pinia

Pinia Basics

3 min read
In this lesson5

Welcome to the NOVA LAB Central Database! Year 2087 - this is where we store all critical data from the Mars mission. Just as the base's main computer stores information about systems, crew, and resources, Pinia stores the global state of a Vue application.

What is Pinia?

Pinia is the official state management library for Vue 3. It is the successor to Vuex and offers much simpler syntax. Think of it as the central computer of NOVA LAB - a single control point from which all station modules retrieve data.

Main advantages of Pinia:

  • Simpler syntax than Vuex (no mutations!)
  • Full TypeScript support
  • Great integration with Vue DevTools
  • Modular architecture - multiple independent stores

Installation and Setup

To start Pinia, we need two steps - install the package and connect it to the application:

1npm install pinia
1// main.js
2import { createApp } from 'vue'
3import { createPinia } from 'pinia'
4import App from './App.vue'
5
6const pinia = createPinia()
7const app = createApp(App)
8
9app.use(pinia)
10app.mount('#app')

The createPinia() function creates a Pinia instance, and app.use(pinia) registers it as a Vue plugin. From this point on, all stores are available throughout the application.

Defining a Store - defineStore

A store is created using defineStore. It takes two arguments: a unique store name and a setup function (Composition API style):

1// stores/mission.js
2import { defineStore } from 'pinia'
3import { ref, computed } from 'vue'
4
5export const useMissionStore = defineStore('mission', () => {
6  // State - reactive data
7  const missionName = ref('Mars Exploration Alpha')
8  const status = ref('active')
9  const crewCount = ref(6)
10
11  // Getters - computed values
12  const statusLabel = computed(() =>
13    status.value === 'active' ? 'ACTIVE' : 'INACTIVE'
14  )
15
16  // Actions - functions that modify state
17  function updateStatus(newStatus) {
18    status.value = newStatus
19  }
20
21  // Return everything that should be accessible
22  return {
23    missionName,
24    status,
25    crewCount,
26    statusLabel,
27    updateStatus
28  }
29})

Naming convention: useXxxStore - prefix use and suffix Store.

Using the Store in a Component

1<script setup>
2import { useMissionStore } from '@/stores/mission'
3
4const missionStore = useMissionStore()
5
6// Access state
7console.log(missionStore.missionName)
8
9// Call an action
10missionStore.updateStatus('completed')
11</script>
12
13<template>
14  <div>
15    <h1>{{ missionStore.missionName }}</h1>
16    <p>Status: {{ missionStore.statusLabel }}</p>
17    <button @click="missionStore.updateStatus('paused')">
18      Pause Mission
19    </button>
20  </div>
21</template>

storeToRefs - Reactive Destructuring

Regular destructuring loses reactivity. Use storeToRefs for state and getters:

1<script setup>
2import { useMissionStore } from '@/stores/mission'
3import { storeToRefs } from 'pinia'
4
5const store = useMissionStore()
6
7// State and getters - storeToRefs (preserves reactivity)
8const { missionName, status, statusLabel } = storeToRefs(store)
9
10// Actions - regular destructuring (they're functions, not refs)
11const { updateStatus } = store
12</script>
Code for this lesson: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// Pinia store simulation (Composition API style)
5// In a real app: import { defineStore } from 'pinia'
6
7// State
8const missionName = ref('Mars Exploration Alpha')
9const status = ref('active')
10const crewCount = ref(6)
11const fuelLevel = ref(87)
12
13// Getters
14const statusLabel = computed(() =>
15  status.value === 'active' ? 'AKTYWNA' : 'NIEAKTYWNA'
16)
17
18const fuelStatus = computed(() => {
19  if (fuelLevel.value > 70) return 'Optimal'
20  if (fuelLevel.value > 30) return 'Moderate'
21  return 'Critical'
22})
23
24// Actions
25function toggleStatus() {
26  status.value = status.value === 'active' ? 'paused' : 'active'
27}
28
29function consumeFuel(amount) {
30  fuelLevel.value = Math.max(0, fuelLevel.value - amount)
31}
32
33function refuel() {
34  fuelLevel.value = 100
35}
36</script>
37
38<template>
39  <div class="nova-lab">
40    <h1>NOVA LAB - Pinia Basics</h1>
41
42    <div class="panel">
43      <h2>Mission Store</h2>
44      <div class="stat">
45        <span class="label">Mission:</span>
46        <span>{{ missionName }}</span>
47      </div>
48      <div class="stat">
49        <span class="label">Status:</span>
50        <span :class="status">{{ statusLabel }}</span>
51      </div>
52      <div class="stat">
53        <span class="label">Crew:</span>
54        <span>{{ crewCount }} members</span>
55      </div>
56      <div class="stat">
57        <span class="label">Fuel:</span>
58        <div class="fuel-bar">
59          <div class="fuel-fill" :style="{ width: fuelLevel + '%' }" :class="fuelStatus.toLowerCase()"></div>
60        </div>
61        <span>{{ fuelLevel }}% ({{ fuelStatus }})</span>
62      </div>
63
64      <div class="actions">
65        <button @click="toggleStatus" class="btn">
66          {{ status === 'active' ? 'Pause' : 'Resume' }}
67        </button>
68        <button @click="consumeFuel(10)" class="btn btn-warn">
69          Consume Fuel (-10)
70        </button>
71        <button @click="refuel" class="btn btn-success">
72          Refuel
73        </button>
74      </div>
75    </div>
76  </div>
77</template>
78
79<style scoped>
80.nova-lab {
81  background: #0a0e27;
82  color: #00ff88;
83  min-height: 100vh;
84  padding: 20px;
85  font-family: monospace;
86}
87h1 { color: #00ff88; margin-bottom: 20px; text-align: center; }
88.panel {
89  background: rgba(0, 180, 216, 0.1);
90  border: 2px solid #00b4d8;
91  padding: 20px;
92  max-width: 500px;
93  margin: 0 auto;
94}
95h2 { color: #00b4d8; margin-bottom: 15px; }
96.stat {
97  display: flex;
98  align-items: center;
99  gap: 10px;
100  margin: 10px 0;
101  flex-wrap: wrap;
102}
103.label { color: #00b4d8; min-width: 80px; }
104.active { color: #00ff88; font-weight: bold; }
105.paused { color: #ff9900; font-weight: bold; }
106.fuel-bar {
107  width: 150px;
108  height: 16px;
109  background: #1a1e3f;
110  border: 1px solid #00b4d8;
111}
112.fuel-fill {
113  height: 100%;
114  transition: width 0.3s;
115}
116.fuel-fill.optimal { background: #00ff88; }
117.fuel-fill.moderate { background: #ff9900; }
118.fuel-fill.critical { background: #ff4444; }
119.actions {
120  display: flex;
121  gap: 8px;
122  margin-top: 15px;
123  flex-wrap: wrap;
124}
125.btn {
126  background: #00b4d8;
127  color: #0a0e27;
128  border: none;
129  padding: 8px 16px;
130  cursor: pointer;
131  font-family: monospace;
132  font-weight: bold;
133}
134.btn:hover { background: #00ff88; }
135.btn-warn { background: #ff9900; }
136.btn-warn:hover { background: #ffbb33; }
137.btn-success { background: #00ff88; }
138.btn-success:hover { background: #33ffaa; }
139</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. What is Pinia in the Vue 3 ecosystem?

  2. 2. How do you create a Pinia instance in main.js?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Create your first Pinia store with state, getters, and actions

  • Click in order

    Arrange the elements to correctly call defineStore

  • Code editor

    Use a Pinia store in a Vue component

Useful articles