Cours Vue.js · Module 11: Pinia

Stores en Composition API (syntaxe setup)

3 min de lecture
Dans cette leçon5

Au NOVA LAB, chaque système possède son propre protocole de configuration. Pinia propose deux styles pour définir les stores - la Composition API (fonction setup) et l'Options API. La Composition API est recommandée car elle est plus proche de la façon dont on écrit les composants Vue 3.

Composition API vs Options API

1// Composition API (RECOMMANDÉE)
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})

Quand choisir quel style ?

AspectComposition APIOptions API
FlexibilitéTotaleLimitée
TypeScriptTypage naturelNécessite des déclarations
$reset()À définir manuellementFonctionne automatiquement
ComposablesFaciles à utiliserPlus difficile
SyntaxeComme <script setup>Comme l'Options API de Vue

Utiliser des composables dans un store

La Composition API permet d'utiliser facilement des composables à l'intérieur d'un store :

1import { defineStore } from 'pinia'
2import { ref, computed, watch } from 'vue'
3import { useLocalStorage } from '@vueuse/core'
4
5export const useSettingsStore = defineStore('settings', () => {
6  // Un composable à l'intérieur d'un 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 dans un store

Tu peux utiliser watch et watchEffect à l'intérieur d'un store :

1export const useAlertStore = defineStore('alerts', () => {
2  const temperature = ref(22)
3  const alerts = ref([])
4
5  // Watcher réagissant aux changements
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})

Typage avec 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 de cette leçon : App.vue
1<script setup>
2import { ref, computed, watch } from 'vue'
3
4// Store Composition API avec watchers et patterns TypeScript
5const temperature = ref(22)
6const alerts = ref([])
7const settings = ref({
8  theme: 'dark',
9  language: 'fr',
10  unit: 'celsius'
11})
12
13// Watcher à l'intérieur du 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>

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quelle est la principale différence entre un store en Composition API et en Options API dans Pinia ?

  2. 2. Peut-on utiliser watch() à l'intérieur d'un store en Composition API ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les éléments d'un store en Composition API dans le bon ordre

  • Éditeur de code

    Crée un store de paramètres en utilisant la Composition API

  • Tri horizontal

    Ordonne les éléments pour typer un store avec TypeScript

  • Éditeur de code

    Crée un store complet de gestion de l'équipage