Gestion d'état et persistance des données

Dans le chapitre précédent, nous avons terminé la composantisation. Nous faisons maintenant face au dernier problème concret :Rafraîchissement de la page, toutes les données sont perdues.。

Pour résoudre ce problème, nous allons introduire la bibliothèque de gestion d'état officiellement recommandée par VuePinia, et combinerTailwind v4avec les capacités de style, afin d'obtenir des donnéesstockées de manière persistante。


Gestion d'état et persistance des données avec Pinia

Pinia est une bibliothèque légère de gestion d'état pour Vue.js. Elle vous permet de partager et de gérer l'état entre les composants. On peut considérer Pinia comme un store de données global ; tous les composants peuvent y récupérer des données ou y mettre à jour des données.

Référence pour le contenu lié à Pinia :Tutoriel d'introduction à Pinia。

Pourquoi utiliser Pinia ?

Dans le code précédent, les données se trouvaient toutesApp.vuelà-dedans. Si le projet grandit (par exemple, ajout d'une page de statistiques, d'un espace utilisateur), le passage de données entre composants devient un cauchemar à la poupée russe (Props Hell).

  • Piniafournit un store de données global.
  • N'importe quel composant peut directement obtenir des données depuis le store, ou déclencher des méthodes sur le store.

Installation et initialisation :

npm install pinia
npm install @vue/devtools-kit -D

Enregistrer dans src/main.js :

Exemple

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import './style.css'

const app = createApp(App)
app.use(createPinia()) // Enregistrement du plugin
app.mount('#app')

Créer le store de tâches (store)

Créez un répertoire `stores` sous le répertoire `src`, puis créez `taskStore.js` sous `src/stores`.

Nous allons déplacer ici toute la logique qui se trouvait à l'origine dans App.vue.

Exemple

import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'

export const useTaskStore = defineStore('task-store', () => {
  // --- 1. État ---
  // Essayer de lire la valeur initiale depuis LocalStorage
  const savedTasks = localStorage.getItem('my-tasks')
  const tasks = ref(savedTasks ? JSON.parse(savedTasks) : [])
  const filter = ref('all')

  // --- 2. État dérivé (Getters) ---
  const filteredTasks = computed(() => {
    if (filter.value === 'active') return tasks.value.filter(t => !t.isCompleted)
    if (filter.value === 'completed') return tasks.value.filter(t => t.isCompleted)
    return tasks.value
  })

  // --- 3. Actions ---
  const addTask = (title) => {
    tasks.value.unshift({ id: crypto.randomUUID(), title, isCompleted: false })
  }

  const removeTask = (id) => {
    tasks.value = tasks.value.filter(t => t.id !== id)
  }

  const toggleTask = (id) => {
    const task = tasks.value.find(t => t.id === id)
    if (task) task.isCompleted = !task.isCompleted
  }

  // --- 4. Persistance ---
  // Surveiller les changements dans les tâches, dès qu'un changement se produit, écrire dans LocalStorage
  watch(tasks, (newVal) => {
    localStorage.setItem('my-tasks', JSON.stringify(newVal))
  }, { deep: true }) // Observer en profondeur les modifications des objets à l'intérieur du tableau

  return {
    tasks,
    filter,
    filteredTasks,
    addTask,
    removeTask,
    toggleTask
  }
})

Refactoriser App.vue (Connexion au Store)

Désormais, App.vue n'a plus besoin de gérer les données lui-même ; il lui suffit d'appeler le Store.

Exemple

<script setup>
import { storeToRefs } from 'pinia'
import { useTaskStore } from '@/stores/taskStore'
import TaskHeader from './components/TaskHeader.vue'
import TaskInput from './components/TaskInput.vue'
import TaskFilter from './components/TaskFilter.vue'
import TaskItem from './components/TaskItem.vue'

// Initialiser le store
const taskStore = useTaskStore()

// Utiliser storeToRefs pour conserver la déstructuration réactive des données
// Ainsi, vous pouvez utiliser directement filter et filteredTasks dans le template
const { filter, filteredTasks } = storeToRefs(taskStore)
const { addTask, removeTask, toggleTask } = taskStore
</script>

<template>
  <div class="min-h-screen py-12 px-4">
    <div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl border border-slate-100 overflow-hidden">
      <TaskHeader />
      <main class="p-6">
        <TaskInput @add-task="addTask" />
        <TaskFilter v-model="filter" />

        <ul class="space-y-3">
          <TransitionGroup name="list">
            <TaskItem
             v-for="task in filteredTasks"
             :key="task.id"
             :task="task"
             @toggle="toggleTask"
             @remove="removeTask"
           />
          </TransitionGroup>
        </ul>
      </main>
    </div>
  </div>
</template>

Explication détaillée des notions

1. storeToRefsQu'est-ce que c'est ?

Si vous l'écrivez directement comme ceci :const { filter } = taskStore, vous perdrez la réactivité. Lorsque vous modifiezfilter, la page ne s'actualisera pas.

  • Raison: Le store est unreactiveobjet encapsulé ; la déstructuration directe brisera la référence.
  • Solution: UtiliserstoreToRefs. Il convertira l'état du store enref, garantissant qu'après la déstructuration, il peut toujours « déplacer tout le corps en tirant un seul cheveu ».

2. watchSurveillance profonde (deep: true)

Dans la logique de persistance, ce que nous surveillons esttasksle tableau.

  • Si vous ne l'ajoutez pas{ deep: true }, ce n'est que lorsque le tableau est remplacé (par exemple,tasks.value = []) que l'enregistrement sera déclenché.
  • Après l'avoir ajouté, les modifications apportées aux propriétés des objets à l'intérieur du tableau (par exemple, une tâche passant de "pending" à "completed") seront également capturées et enregistrées sur le disque.

3. Approche de persistance

Nous utilisons une boucle fermée de "lecture à l'initialisation + écriture en cas de changement" :

  1. Lecture: Lorsque le Store est créé, à partir delocalStoragela récupération des données, donnant à l'application une "mémoire".
  2. Écriture: Utilisezwatchpour parvenir à une synchronisation entièrement automatisée, les développeurs n'ont plus besoin d'écrire manuellementsetItem。

Finitions finales avec Tailwind v4

Avec le support du Store, nous pouvons ajouter quelquesstyles de retour globaux. Par exemple, dans leTaskHeader.vueafficher la progression des tâches :

Exemple

<script setup>
<script setup>
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useTaskStore } from '@/stores/taskStore'; // Assurez-vous que le chemin est correct

// 1. Initialiser le store
const taskStore = useTaskStore();

// 2. Utiliser storeToRefs pour déstructurer tasks, en veillant à ce que ce soit réactif
// Si vous faites directement const { tasks } = taskStore; la barre de progression ne bougera pas lorsque les éléments sont cochés
const { tasks } = storeToRefs(taskStore);

// 3. Écrire la logique de progression
const progress = computed(() => {
  const total = tasks.value.length;
if (total === 0) return 0; // Éviter la division par zéro

  const completedCount = tasks.value.filter(t => t.isCompleted).length;
// Calculer le pourcentage et l'arrondir
  return Math.round((completedCount / total) * 100);
});
</script>

<template>
  <header class="bg-linear-to-br from-blue-600 to-indigo-700 p-8 text-white">
    <h1 class="text-3xl font-black tracking-tight">TaskHub</h1>
   
    <div class="mt-6">
      <div class="flex justify-between items-end mb-2">
        <p class="text-blue-100/80 text-xs font-bold uppercase tracking-wider">Progression d'achèvement</p>
        <span class="text-2xl font-mono font-black">{{ progress }}%</span>
      </div>
     
      <div class="h-2 w-full bg-white/20 rounded-full overflow-hidden backdrop-blur-sm">
        <div
         class="h-full bg-white shadow-[0_0_15px_rgba(255,255,255,0.5)] transition-all duration-500 ease-out"
         :style="{ width: `${progress}%` }"
       ></div>
      </div>
    </div>
  </header>
</template>

Après la modification, toute l'interface dispose désormais de la fonctionnalité de barre de progression :

Autres extensions