Gestión de estado y persistencia de datos

En el capítulo anterior ya completamos la componentización; ahora nos enfrentamos al último punto problemático práctico:Al actualizar la página, todos los datos se pierden。

Para resolver este problema, introduciremos la biblioteca de gestión de estado recomendada oficialmente por VuePinia, y combinándola conTailwind v4la capacidad de estilos de, para lograr lapersistencia de almacenamiento。


Gestión de estado de Pinia y persistencia de datos

Pinia es una biblioteca ligera de gestión de estado para Vue.js que te permite compartir y gestionar estado entre componentes. Podemos imaginar a Pinia como un almacén de datos global del que todos los componentes pueden obtener o actualizar datos.

Referencia relacionada con Pinia:Tutorial de introducción a Pinia。

¿Por qué usar Pinia?

En el código anterior, todos los datos estaban enApp.vuedentro. Si el proyecto crece (por ejemplo, añadiendo una página de estadísticas o un centro de usuario), pasar datos entre componentes se convierte en una pesadilla de muñecas rusas (Props Hell).

  • PiniaProporciona un almacén de datos global.
  • Cualquier componente puede obtener datos directamente del almacén o invocar métodos del almacén.

Instalación e inicialización:

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

Regístralo en src/main.js:

Ejemplo

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

const app = createApp(App)
app.use(createPinia()) // Registro de plugins
app.mount('#app')

Crear Task Store (almacén)

Crea el directorio stores dentro de src y luego crea taskStore.js en el directorio src/stores.

Vamos a mover toda la lógica que estaba originalmente en App.vue a este archivo.

Ejemplo

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

export const useTaskStore = defineStore('task-store', () => {
  // --- 1. Estado (State) ---
  // Intentar leer el valor inicial desde LocalStorage
  const savedTasks = localStorage.getItem('my-tasks')
  const tasks = ref(savedTasks ? JSON.parse(savedTasks) : [])
  const filter = ref('all')

  // --- 2. Estado derivado (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. Acciones (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. Persistencia (Persistence) ---
  // Escuchar los cambios de tasks y escribirlos en LocalStorage cuando cambien
  watch(tasks, (newVal) => {
    localStorage.setItem('my-tasks', JSON.stringify(newVal))
  }, { deep: true }) // Escucha profunda de los cambios de los objetos dentro del array

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

Refactorizar App.vue (conectar al almacén)

Ahora App.vue ya no necesita gestionar los datos por sí mismo; solo tiene que usar el Store.

Ejemplo

<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'

// Inicializar el almacén
const taskStore = useTaskStore()

// Usar storeToRefs para mantener la desestructuración reactiva de los datos
// Así, en la plantilla se pueden usar directamente filter y filteredTasks
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>

Explicación detallada de los puntos de conocimiento

1. storeToRefs¿Qué es?

Si escribes directamente así:const { filter } = taskStore, perderás la reactividad. Cuando modifiquesfilter, la página no se actualizará.

  • Razón: Store es un objetoreactiveenvuelto con [reactive]; desestructurarlo directamente romperá la referencia.
  • Solución: UsastoreToRefs. Convertirá el estado del almacén enref, asegurando que después de desestructurar, un cambio siga afectando a todo el sistema.

2. watchLa escucha profunda (deep: true)

En la lógica de persistencia, lo que escuchamos estasksarray.

  • Si no añadimos{ deep: true }, solo cuando el array sea reemplazado (por ejemplo,tasks.value = []) se activará el guardado.
  • Si lo añadimos, las modificaciones de propiedades de los objetos dentro del array (por ejemplo, una tarea que pasa de "pendiente" a "completada") también se capturarán y se guardarán en el disco.

3. Enfoque de persistencia

Hemos adoptado un ciclo cerrado de "lectura en la inicialización + escritura en el cambio":

  1. Lectura: Cuando se crea el Store, desdelocalStoragese obtienen los datos, dando "memoria" a la aplicación.
  2. Escritura: Usandowatchse logra una sincronización completamente automática; los desarrolladores ya no necesitan escribir manualmentesetItem。

El pulido final con Tailwind v4

Con la ayuda del Store, podemos añadir a la aplicación algunosestilos de retroalimentación global. Por ejemplo, enTaskHeader.vuese muestra el progreso de las tareas:

Ejemplo

<script setup>
<script setup>
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useTaskStore } from '@/stores/taskStore'; // Asegúrate de que la ruta sea correcta

// 1. Inicializar el store
const taskStore = useTaskStore();

// 2. Usar storeToRefs para desestructurar tasks, asegurando que sea reactivo
// Si haces const { tasks } = taskStore; directamente, la barra de progreso no se moverá al marcar las casillas
const { tasks } = storeToRefs(taskStore);

// 3. Escribir la lógica de progreso
const progress = computed(() => {
  const total = tasks.value.length;
if (total === 0) return 0; // Evitar la división por cero

  const completedCount = tasks.value.filter(t => t.isCompleted).length;
// Calcular el porcentaje y redondear
  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">Progreso de finalización</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>

Después de la modificación, toda la interfaz tendrá la funcionalidad de barra de progreso:

Otras extensiones