Now let’s assume that we want to build a simple to-do app using both tools:
Pinia
Let’s take a look at Pinia first. Below you can see an example of how to create an independent Pinia store module using TypeScript with Vue3’s composition api and how to utilize it in your component.In this module I’ve declared the state as ref’s (tasks, filter), getters as computed properties (filteredTasks) and functions as actions (addTask, removeTask, toggleTask). I also added state persistence by taking advantage of a watcher, local storage and the onMounted lifecycle hook.
// store/tasks.ts
import { defineStore } from "pinia";
import { ref, computed, watch, onMounted } from "vue";
// Define the Task type
interface Task {
id: number;
title: string;
completed: boolean;
}
// Store definition
export const useTaskStore = defineStore("tasks", () => {
const tasks = ref<Task[]>([
{ id: 1, title: "Task 1", completed: false },
{ id: 2, title: "Task 2", completed: true },
]);
const filter = ref<"all" | "completed" | "incomplete">("all");
// Getters
const filteredTasks = computed((): Task[] => {
if (filter.value === "completed") {
return tasks.value.filter((task) => task.completed);
} else if (filter.value === "incomplete") {
return tasks.value.filter((task) => !task.completed);
}
return tasks.value;
});
// Actions
const addTask = (title: string) => {
tasks.value.push({
id: Date.now(),
title,
completed: false,
});
};
const removeTask = (id: number) => {
tasks.value = tasks.value.filter((task) => task.id !== id);
};
const toggleTask = (id: number) => {
console.log("id", id);
const task = tasks.value.find((task) => task.id === id);
if (task) {
task.completed = !task.completed;
}
};
// Persist changes to localStorage
watch(
tasks,
(newTasks) => {
localStorage.setItem("tasks", JSON.stringify(newTasks));
},
{ deep: true }
);
onMounted(() => {
const storedTasks = localStorage.getItem("tasks");
if (storedTasks) {
tasks.value = JSON.parse(storedTasks);
}
});
return { tasks, filter, filteredTasks, addTask, removeTask, toggleTask };
});
// TodoPinia.vue
<template>
<div class="todo">
<select v-model="taskStore.filter">
<option value="all">All</option>
<option value="completed">Completed</option>
<option value="incomplete">Incomplete</option>
</select>
<ul>
<li v-for="task in taskStore.filteredTasks" :key="task.id">
<input
type="checkbox"
v-model="task.completed"
@update="taskStore.toggleTask(task.id)"
/>
{{ task.title }}
<button @click="taskStore.removeTask(task.id)">X</button>
</li>
</ul>
<input
v-model="newTaskTitle"
@keyup.enter="addTask"
placeholder="New task"
/>
<button @click="addTask">Add Task</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useTaskStore } from "../store/pinia/tasks";
const taskStore = useTaskStore();
const newTaskTitle = ref<string>("");
const addTask = () => {
if (newTaskTitle.value.trim()) {
taskStore.addTask(newTaskTitle.value);
newTaskTitle.value = "";
}
};
</script>
Vuex
And here goes Vuex. Here, I have recreated basically the same store module as in Pinia. For the purpose of this article, I have simplified things by not creating a root store instance in which I’d have to import the module as you would normally do. This is an extra step for when we need to have more than one Vuex store module. You’ll notice some differences - the clunkier syntax, more extensive typing, and a dedicated plugin needed for state persistence.
// store/vuex/tasks.ts
import { createStore, Store } from "vuex";
import createPersistedState from "vuex-persistedstate";
import type { InjectionKey } from "vue";
// Define the Task type
export interface Task {
id: number;
title: string;
completed: boolean;
}
// Define the Tasks module state type
export interface TasksState {
tasks: Task[];
filter: "all" | "completed" | "incomplete";
}
const persistedTasks = createPersistedState({
paths: ["tasks"],
});
// Export injection key for better typing with useStore
export const key: InjectionKey<Store<TasksState>> = Symbol();
export const store = createStore<TasksState>({
plugins: [persistedTasks],
// State
state: {
tasks: [
{ id: 1, title: "Task 1", completed: false },
{ id: 2, title: "Task 2", completed: true },
],
filter: "all",
},
// Getters
getters: {
filteredTasks(state): Task[] {
if (state.filter === "completed") {
return state.tasks.filter((task) => task.completed);
} else if (state.filter === "incomplete") {
return state.tasks.filter((task) => !task.completed);
}
return state.tasks;
},
},
// Mutations
mutations: {
addTask(state, title: string) {
state.tasks.push({
id: Date.now(),
title,
completed: false,
});
},
removeTask(state, id: number) {
state.tasks = state.tasks.filter((task) => task.id !== id);
},
toggleTask(state, id: number) {
const task = state.tasks.find((task) => task.id === id);
if (task) {
task.completed = !task.completed;
}
},
setFilter(state, filter: "all" | "completed" | "incomplete") {
state.filter = filter;
},
},
// Actions
actions: {
addTask({ commit }, title: string) {
commit("addTask", title);
},
toggleTask({ commit }, id: number) {
commit("toggleTask", id);
},
setFilter({ commit }, filter: "all" | "completed" | "incomplete") {
commit("setFilter", filter);
},
},
});
// TodoVuex.vue
<template>
<div class="todo">
<select
v-model="store.state.filter"
@update:modelValue="store.commit('setFilter', $event)"
>
<option value="all">All</option>
<option value="completed">Completed</option>
<option value="incomplete">Incomplete</option>
</select>
<ul>
<li v-for="task in store.getters.filteredTasks" :key="task.id">
<input
type="checkbox"
v-model="task.completed"
@update="store.commit('toggleTask', task.id)"
/>
{{ task.title }}
<button @click="store.commit('removeTask', task.id)">X</button>
</li>
</ul>
<input
v-model="newTaskTitle"
@keyup.enter="addTask"
placeholder="New task"
/>
<button @click="addTask">Add Task</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useStore } from "vuex";
import { type TasksState, key } from "../store/vuex/tasks";
const store = useStore<TasksState>(key);
const newTaskTitle = ref("");
const addTask = () => {
if (newTaskTitle.value.trim()) {
store.commit("addTask", newTaskTitle.value);
newTaskTitle.value = "";
}
};
</script>