Vue.js course ยท Module 11: Pinia

Composition API Stores (setup syntax)

3 min read
In this lesson5

At NOVA LAB, every system has its own configuration protocol. Pinia offers two styles for defining stores - Composition API (setup function) and Options API. Composition API is recommended because it's closer to how we write Vue 3 components.

Composition API vs Options API

1// Composition API (RECOMMENDED)
2export const useCounterStore = defineStore('counter', () => {
3  const count = ref(0)
4  const doubleCount = computed(() => count.value * 2)
5  function increment() { count.value++ }
6
7  return { count, doubleCount, increment }
8})
9
10// Options API
11export const useCounterStore = defineStore('counter', {
12  state: () => ({ count: 0 }),
13  getters: {
14    doubleCount: (state) => state.count * 2
15  },
16  actions: {
17    increment() { this.count++ }
18  }
19})

When to Choose Which Style?

AspectComposition APIOptions API
FlexibilityFullLimited
TypeScriptNatural typingRequires declarations
$reset()Must define manuallyWorks automatically
ComposablesEasy to useMore difficult
SyntaxLike <script setup>Like Vue Options API

Using Composables in a Store

Composition API allows easy use of composables inside a store:

1import { defineStore } from 'pinia'
2import { ref, computed, watch } from 'vue'
3import { useLocalStorage } from '@vueuse/core'
4
5export const useSettingsStore = defineStore('settings', () => {
6  // Composable inside a store!
7  const theme = useLocalStorage('theme', 'dark')
8  const language = useLocalStorage('language', 'en')
9
10  const isDark = computed(() => theme.value === 'dark')
11
12  function toggleTheme() {
13    theme.value = isDark.value ? 'light' : 'dark'
14  }
15
16  return { theme, language, isDark, toggleTheme }
17})

Watchers in a Store

You can use watch and watchEffect inside a store:

1export const useAlertStore = defineStore('alerts', () => {
2  const temperature = ref(22)
3  const alerts = ref([])
4
5  // Watcher reacting to changes
6  watch(temperature, (newTemp, oldTemp) => {
7    if (newTemp > 50) {
8      alerts.value.push({
9        type: 'critical',
10        message: `Temperature critical: ${newTemp}ยฐC`,
11        timestamp: Date.now()
12      })
13    }
14  })
15
16  function setTemperature(value) {
17    temperature.value = value
18  }
19
20  return { temperature, alerts, setTemperature }
21})

Typing with TypeScript

1interface CrewMember {
2  id: number
3  name: string
4  role: 'commander' | 'engineer' | 'scientist'
5  active: boolean
6}
7
8export const useCrewStore = defineStore('crew', () => {
9  const members = ref<CrewMember[]>([])
10  const selected = ref<CrewMember | null>(null)
11
12  const activeMembers = computed<CrewMember[]>(() =>
13    members.value.filter(m => m.active)
14  )
15
16  function addMember(member: Omit<CrewMember, 'id'>): CrewMember {
17    const newMember: CrewMember = {
18      ...member,
19      id: Date.now()
20    }
21    members.value.push(newMember)
22    return newMember
23  }
24
25  return { members, selected, activeMembers, addMember }
26})
Code for this lesson: App.vue
1<script setup>
2import { ref, computed, watch } from 'vue'
3
4// Composition API Store with watchers and TypeScript patterns
5const temperature = ref(22)
6const alerts = ref([])
7const settings = ref({
8  theme: 'dark',
9  language: 'pl',
10  unit: 'celsius'
11})
12
13// Watcher inside store
14watch(temperature, (newTemp) => {
15  if (newTemp > 45) {
16    alerts.value.push({
17      id: Date.now(),
18      type: 'critical',
19      message: `Temperature critical: ${newTemp} C`,
20      time: new Date().toLocaleTimeString()
21    })
22  } else if (newTemp > 35) {
23    alerts.value.push({
24      id: Date.now(),
25      type: 'warning',
26      message: `Temperature high: ${newTemp} C`,
27      time: new Date().toLocaleTimeString()
28    })
29  }
30})
31
32// Getters
33const isDark = computed(() => settings.value.theme === 'dark')
34const tempDisplay = computed(() => {
35  if (settings.value.unit === 'fahrenheit') {
36    return ((temperature.value * 9/5) + 32).toFixed(1) + ' F'
37  }
38  return temperature.value + ' C'
39})
40
41const criticalAlerts = computed(() =>
42  alerts.value.filter(a => a.type === 'critical')
43)
44
45// Actions
46function setTemperature(value) {
47  temperature.value = Math.max(-100, Math.min(100, value))
48}
49
50function toggleTheme() {
51  settings.value.theme = isDark.value ? 'light' : 'dark'
52}
53
54function toggleUnit() {
55  settings.value.unit = settings.value.unit === 'celsius' ? 'fahrenheit' : 'celsius'
56}
57
58function dismissAlert(id) {
59  alerts.value = alerts.value.filter(a => a.id !== id)
60}
61
62function clearAlerts() {
63  alerts.value = []
64}
65</script>
66
67<template>
68  <div class="nova-lab" :class="settings.theme">
69    <h1>NOVA LAB - Composition Store</h1>
70
71    <div class="temp-control">
72      <h2>Temperature Control</h2>
73      <div class="temp-display" :class="{ critical: temperature > 45, warning: temperature > 35 }">
74        {{ tempDisplay }}
75      </div>
76      <div class="slider-row">
77        <button @click="setTemperature(temperature - 5)" class="btn">-5</button>
78        <input type="range" v-model.number="temperature" min="-50" max="80" class="slider" />
79        <button @click="setTemperature(temperature + 5)" class="btn">+5</button>
80      </div>
81    </div>
82
83    <div class="settings-panel">
84      <h2>Settings (composable pattern)</h2>
85      <button @click="toggleTheme" class="btn">
86        Theme: {{ settings.theme }}
87      </button>
88      <button @click="toggleUnit" class="btn">
89        Unit: {{ settings.unit }}
90      </button>
91    </div>
92
93    <div v-if="alerts.length" class="alerts-panel">
94      <div class="alerts-header">
95        <h2>Alerts ({{ alerts.length }})</h2>
96        <button @click="clearAlerts" class="btn btn-sm">Clear All</button>
97      </div>
98      <div v-for="alert in alerts" :key="alert.id"
99        class="alert-item" :class="alert.type">
100        <span>{{ alert.time }} - {{ alert.message }}</span>
101        <button @click="dismissAlert(alert.id)" class="dismiss">x</button>
102      </div>
103    </div>
104  </div>
105</template>
106
107<style scoped>
108.nova-lab {
109  min-height: 100vh;
110  padding: 20px;
111  font-family: monospace;
112  transition: all 0.3s;
113}
114.nova-lab.dark { background: #0a0e27; color: #00ff88; }
115.nova-lab.light { background: #f0f4f8; color: #1a1e3f; }
116h1 { margin-bottom: 20px; text-align: center; }
117h2 { color: #00b4d8; margin-bottom: 10px; }
118.temp-control, .settings-panel, .alerts-panel {
119  max-width: 500px;
120  margin: 0 auto 15px;
121  padding: 15px;
122  border: 2px solid #00b4d8;
123  background: rgba(0, 180, 216, 0.1);
124}
125.temp-display {
126  text-align: center;
127  font-size: 2em;
128  font-weight: bold;
129  margin: 10px 0;
130}
131.temp-display.warning { color: #ff9900; }
132.temp-display.critical { color: #ff4444; }
133.slider-row { display: flex; align-items: center; gap: 10px; }
134.slider { flex: 1; }
135.btn {
136  background: #00b4d8;
137  color: #0a0e27;
138  border: none;
139  padding: 6px 14px;
140  cursor: pointer;
141  font-family: monospace;
142  margin: 2px;
143}
144.btn:hover { background: #00ff88; }
145.btn-sm { padding: 4px 10px; font-size: 0.85em; }
146.alerts-header { display: flex; justify-content: space-between; align-items: center; }
147.alert-item {
148  display: flex;
149  justify-content: space-between;
150  align-items: center;
151  padding: 8px;
152  margin: 5px 0;
153  border: 1px solid;
154}
155.alert-item.warning { border-color: #ff9900; color: #ff9900; }
156.alert-item.critical { border-color: #ff4444; color: #ff4444; }
157.dismiss {
158  background: none;
159  border: 1px solid currentColor;
160  color: inherit;
161  cursor: pointer;
162  padding: 2px 6px;
163  font-family: monospace;
164}
165</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 the main difference between a Composition API and Options API store in Pinia?

  2. 2. Can you use watch() inside a Composition API store?

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

Hands-on tasks in the game

  • Vertical ordering

    Arrange the elements of a Composition API store in the correct order

  • Code editor

    Create a settings store using Composition API

  • Horizontal ordering

    Arrange the elements to type a store with TypeScript

  • Code editor

    Create a complete crew management store

Useful articles