YouTip LogoYouTip

Vue3 Taskhub States

```html

State Management and Data Persistence

The previous chapter completed componentization. We now face the final practical pain point of our project: data loss upon page refresh.

To solve this issue, we will introduce Pinia, the official recommended state management library for Vue, combined with the styling capabilities of Tailwind v4, to implement persistent data storage.


Pinia State Management and Data Persistence

Pinia is a lightweight state management library for Vue.js that allows you to share and manage state between components. You can think of Pinia as a global data warehouse where all components can fetch or update data.

For more information on Pinia, refer to: Pinia Getting Started Tutorial.

Why Use Pinia?

In earlier code, data was all stored within App.vue. As projects grow larger (such as adding statistics pages or user centers), passing data between components becomes a nested nightmare (Props Hell).

  • Pinia provides a global data warehouse.
  • Any component can directly access data from the warehouse or trigger its methods.

Installation and Initialization:

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

Register in src/main.js:

Example

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

const app = createApp(App)

app.use(createPinia()) // Plugin registration
app.mount('#app')

Create Task Store (Warehouse)

Create a stores directory under src, then create a new file taskStore.js inside src/stores.

Image 1

We will move all logic originally located in App.vue here.

Example

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

export const useTaskStore = defineStore('task-store', () => {
  // --- 1. State ---
  // Attempt to read initial values from LocalStorage
  const savedTasks = localStorage.getItem('my-tasks')
  const tasks = ref(savedTasks ? JSON.parse(savedTasks) : [])
  const filter = ref('all')

  // --- 2. 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. Persistence ---
  // Watch changes in tasks and write to LocalStorage when changed
  watch(
    tasks,
    (newVal) => {
      localStorage.setItem('my-tasks', JSON.stringify(newVal))
    },
    { deep: true }
  ) // Deeply watch internal object changes in array

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

Refactor App.vue (Connect to Store)

Now App.vue no longer needs to manage data itselfβ€”it just calls the Store.

Example

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

// Initialize store
const taskStore = useTaskStore()

// Use storeToRefs to maintain reactivity during destructuring
// This allows direct usage of filter and filteredTasks in templates
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>

Detailed Explanation of Concepts

1. What is storeToRefs?

If you write it like this: const { filter } = taskStore, you'll lose reactivity. When modifying filter, the page won't refresh.

  • Reason: The store is an object wrapped by reactive; direct destructuring breaks references.
  • Solution: Use storeToRefs. It converts states in the store into refs, ensuring full reactivity after destructuring.

2. Deep Watching (deep: true) with watch

In persistence logic, we are watching the tasks array.

  • Without { deep: true }, saving would only be triggered when the entire array is replaced (e.g., tasks.value = []).
  • With it enabled, modifications to properties inside objects within the array (like changing a task from "pending" to "completed") will also be captured and saved to disk.

3. Persistence Strategy

We adopted a closed-loop approach: β€œRead at initialization + Write on change”:

  1. Reading: On store creation, retrieve data from localStorage so the application has memory.
  2. Writing: Utilize watch to achieve fully automatic synchronizationβ€”developers no longer need to manually call setItem.

Final Polishing with Tailwind v4

With the power of the Store, we can enhance the application with some global feedback styles. For example, display task progress in TaskHeader.vue:

Example

<script setup>
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useTaskStore } from '@/stores/taskStore'; // Ensure correct path

// 1. Initialize store
const taskStore = useTaskStore();

// 2. Destructure tasks using storeToRefs to ensure reactivity
// If written as const { tasks } = taskStore;, the progress bar won't update dynamically
const { tasks } = storeToRefs(taskStore);

// 3. Implement progress logic
const progress = computed(() => {
  const total = tasks.value.length;
  if (total === 0) return 0; // Prevent division by zero

  const completedCount = tasks.value.filter(t => t.isCompleted).length;
  // Calculate percentage and round
  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">Completion Progress</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>

After modification, the interface gains a functional progress bar:

Image 2

```
← Claude Code CheckpointingClaude Code Control β†’