In this article we will explore and learn how to use the new state management library Pinia alongside Vue3 Composition Api.
In my previous projects in Vuejs i am using Vuex as a statement management. Vuex is still a powerful store manager that provides global state accompanied with previous versions with Vuejs and still be used in Vue3. Of course if you are building a new Vue 3 project you can still use Vuex.
However as per Vuex docs, that mentions that Vuex 3 and 4 will still be maintained. and it’s unlikely to add new functionalities to it.
For this reason the official state management library for Vue has changed to Pinia. Pinia has almost the exact same or enhanced API as Vuex 5. We will explore some of Pinia features in this article using a simple example.
What we will do
We will be building a simple Todo list app using Vue3.
In the command line create a new vue app using vue-cli:
vue create vue3-pinia
You will be prompted to select a preset choose Default (Vue 3) ([Vue 3] babel, eslint) and hit enter.
Installing Pinia
After success installation cd into the project, we will install Pinia:
npm install pinia
now run this command to launch the app
npm run serve
This will launch a development server at localhost :8080
Initialize Pinia Instance
The next step is to provide the Pinia plugin in the Vue app. So open src/main.js and modify as follows:
import { createApp } from 'vue' import {createPinia} from 'pinia'; import App from './App.vue' const pinia = createPinia(); const app = createApp(App); app.use(pinia); app.mount('#app')
As you see in this code we imported the createPinia() function from pinia to create a new Pinia instance. Then invoking the createPinia() and assign it to pinia variable and finally we provide it to Vue using app.use(pinia). That’s it.
Store Structure
Pinia store structure is similar to Vuex store but it also extremely simpler in terms of:
- Pinia has a state object.
- Pinia has getters.
- Pinia has actions.
- Pinia doesn’t have Mutations.
- More compatible with Composition Api.
- No need to define namespaced modules for multiple store.
- Nesting stores inside each other is so easy than Vuex.
Defining Store
Store is defined using defineStore() function from pinia:
import {defineStore} from 'pinia'; export const useTodosStore = defineStore('todos', { });
The defineStore() function accepts two arguments, (unique name, options object). The unique name or the id used to identify the store in the devtools, like namespace name in Vuex.
By convention the return value from defineStore() should be name be prefixed with “use” keyword like useTodosStore, useUser in the same way as composables in Composition Api
Now create a directory src/store to hold our store files. Create a single file that will define the todos store.
src/store/todos.js
import {defineStore} from 'pinia'; export const useTodosStore = defineStore('todos', { state: () => ({ todos: [ { id: 1, title: 'Go Shopping', description: 'Go to the market for shopping' } ], showForm: false, id: null }), getters: { count: state => state.todos.length }, actions: { create(title, description) { const created = {id: this.count + 1, title, description}; this.todos = [created, ...this.todos]; } } });
In this code i declared useTodosStore which have these elements:
- state: a function to define the central state, In this case the todos is any array and initialized it with one item and boolean “showForm” and the current updated “id”.
- getters: to define getters for store, it takes the state as first argument. Here we have one getter “count” to get todos length.
- actions: Actions define operations on the state like Vuex actions. Here i created one action to create a todo.
Using The Store
To consume and use the store in any component, depends on whether you are using the composition Api or options Api. In our example we are using the composition Api.
Create these components first:
- src/components/Todos.vue
- src/components/ListTodos.vue
- src/components/TodoForm.vue
Next open App.vue and include the <Todos /> component like so:
src/App.vue
<template> <Todos /> </template> <script> import Todos from "./components/Todos"; export default { name: 'App', components: { Todos } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: start; color: #2c3e50; margin-top: 60px; } </style>
The <Todos /> component represent the todos page which include all the other components for listing all todos, todo form.
Now open this component and update it like so:
components/Todos.vue
<template> <div class="todos"> <ListTodos v-if="!showForm" /> <TodoForm v-if="showForm" /> </div> </template> <script> import {useTodosStore} from "../store/todos"; import {computed} from "vue"; import ListTodos from "./ListTodos"; import TodoForm from "./TodoForm"; export default { name: "Todos", components: {TodoForm, ListTodos}, setup() { const store = useTodosStore(); const showForm = computed(() => store.$state.showForm); return { showForm: showForm } } } </script>
This page displays all todos. In the top i included the two components <ListTodos /> and <TodoForm />. The most important part in the setup() function which invokes the useTodosStore().
Here i want to access the showForm variable from store, using store.$state.showForm to either show the form or hide it. I made it inside the computed() vue helper so that any change in this variable will be triggered. Finally i returned the showForm from setup.
Displaying All Todos
Update ListTodos.vue
<template> <div class="todos-list"> <div><h2>Todos List</h2> <button type="button" class="create" @click="showCreateForm()">Create Todo</button></div> <ol> <li v-for="todo in todos" :key="todo.id"> <div><h4>{{todo.title}}</h4> <button type="button" class="edit" @click="showUpdateForm(todo.id)">Update</button> <button type="button" class="remove">Remove</button></div> <p>{{todo.description}}</p> </li> </ol> </div> </template> <script> import {computed} from 'vue'; import {useTodosStore} from "../store/todos"; export default { name: "ListTodos", setup() { const store = useTodosStore(); const showCreateForm = () => { store.$patch({ showForm: true, id: null }); } const showUpdateForm = (id) => { store.$patch({ showForm: true, id }); } const todos = computed(() => store.$state.todos); return { todos, showCreateForm, showUpdateForm } } } </script> <style scoped> h4, h2 { display: inline; } button { margin: 5px; border: none; border-radius: 8px; cursor: pointer; font-size: 12px; padding: 4px 6px; } button.edit { background-color: aqua; } button.remove { background-color: red; } button.create { background-color: #1377e2; color: #fff; } p { font-size: 13px; } </style>
This component display a list of todos and button to show the create form. Again in the setup() function invoked useTodosStore().
The showCreateForm(), and showUpdateForm() show the create and update todo form respectively. Both functions have similar logic. Here we are mutating the state. So i used Pinia function “$patch“. The $patch function executes several updates to state in one shot.
store.$patch({ showForm: true, id: null });
You can also mutate the state directly by accessing the state and modify like so:
store.$state.showForm=true; store.$state.id = null;
$patch is preferred when modifying multiple items at once. It’s important to wrap the accessed items inside of computed so that updates happen to state reflected instantly.
Finally i returned the data todos, showCreateForm, showUpdateForm.
Creating New Todo
Open TodoForm.vue and update like so:
<template> <div> <h2>{{!updateId ? 'Create Todo' : 'Update Todo'}}</h2> <form method="post" action="#" @submit.prevent="handleSubmit"> <p> <label>Title</label> <input type="text" name="title" id="title" v-model="form.title" /> </p> <p> <label>Description</label> <textarea name="description" id="description" rows="5" v-model="form.description"></textarea> </p> <div><input type="submit" :value="!updateId ? 'Create' : 'Update'" /> </div> </form> </div> </template> <script> import {reactive, computed} from 'vue'; import {useTodosStore} from "../store/todos"; export default { name: "TodoForm", setup() { const store = useTodosStore(); const updateId = computed(() => store.$state.id) const form = reactive({title: '', description: ''}); const handleSubmit = () => { if(!form.title || !form.description) { alert("Please enter the todo title and description"); return; } store.create(form.title, form.description); store.$patch({ showForm: false }); } return { updateId, form, handleSubmit } } } </script> <style scoped> p { font-weight: bold; } input, textarea { margin-left: 30px; padding: 5px 10px; } textarea { width: 700px; } input[type=submit] { margin: 5px; border: none; border-radius: 8px; cursor: pointer; font-size: 12px; padding: 4px 6px; background-color: #1377e2; color: #fff; } </style>
In the same way i accessed the useTodosStore. I retrieved the current todo id to check if this an update or create form using store.$state.id inside of computed.
I declared a reactive variable “form” to hold the form data. I created a method “handleSubmit” which triggers when submitting the form.
The logic inside the handleSubmit is to invoke an action from the store to create the todo. In this case i am invoked the create action store.create(form.title, form.description). Then i mutated the state using store.$patch to hide the form and display the list of todos.
Updating Todo
The update todo part located in the same <TodoForm /> component so let’s make these updates:
TodoForm.vue
Below this line:
const form = reactive({title: '', description: ''});
Insert this code:
if(updateId.value) { const findById = store.findById; const findForm = findById(updateId.value); form.title = findForm.title; form.description = findForm.description; }
Next modify handleSubmit() function:
const handleSubmit = () => { if(!form.title || !form.description) { alert("Please enter the todo title and description"); return; } if(!updateId.value) { store.create(form.title, form.description); } else { store.update(form.title, form.description); } store.$patch({ showForm: false }); }
Then update the todos store, add new action and getters:
store/todos.js
getters: { count: state => state.todos.length, findById: state => todoId => state.todos.find(todo => todo.id === todoId), findIndex: state => todoId => state.todos.findIndex(todo => todo.id === todoId) }, actions: { create(title, description) { const created = {id: this.count + 1, title, description}; this.todos = [created, ...this.todos]; }, update(title, description) { const index = this.findIndex(this.id); this.todos[index] = {...this.todos[index], title, description}; } }
I added two getters, findById, findIndex and also added new action to update the todo. The getters findById and findByIndex accepts the todoId parameter. To use findById in component:
const findById = store.findById; const findForm = findById(1); form.title = findForm.title;
Removing Todo
To remove todo open ListTodos.vue and make these updates:
Under this function:
const showUpdateForm = (id) => {
Add new function:
const removeTodo = (id) => { store.remove(id); }
Then add to the return statement:
return { todos, showCreateForm, showUpdateForm, removeTodo }
And in the template add the click event for remove like so:
<button type="button" class="remove" @click="removeTodo(todo.id)">Remove</button>
Now let’s add new action in the store:
store/todos.js
actions: { .... .... , remove(id) { this.todos = this.todos.filter(todo => todo.id !== id); } }
That’s it. Now we have a fully working todos app using Pinia store and Vue 3.
The full store source
store/todos.js
import {defineStore} from 'pinia'; const initialState = [ { id: 1, title: 'Go Shopping', description: 'Go to the market for shopping' } ]; export const useTodosStore = defineStore('todos', { state: () => ({ todos: [...initialState], showForm: false, id: null }), getters: { count: state => state.todos.length, findById: state => todoId => state.todos.find(todo => todo.id === todoId), findIndex: state => todoId => state.todos.findIndex(todo => todo.id === todoId) }, actions: { create(title, description) { const created = {id: this.count + 1, title, description}; this.todos = [created, ...this.todos]; }, update(title, description) { const index = this.findIndex(this.id); this.todos[index] = {...this.todos[index], title, description}; }, remove(id) { this.todos = this.todos.filter(todo => todo.id !== id); } } });
Another Approach To Define Store(Setup Stores)
The store we created in the previous section is called option store which is similar to the Options Api in components which we defined the state, getters, and actions as an object.
However Pinia also allow us to declare a store using another way, instead of providing an options object when calling defineStore() we can pass a setup function like so:
export const useTodosStore = defineStore('todos', () => { // setup function });
Inside the setup function we can use a similar methods like the setup() function in components composition Api.
Let’s refactor the above store as a setup store
store/todos.js
import {ref, computed} from 'vue'; import {defineStore} from 'pinia'; const initialState = [ { id: 1, title: 'Go Shopping', description: 'Go to the market for shopping' } ]; export const useTodosStore = defineStore('todos', () => { // state const todos = ref(initialState); const showForm = ref(false); const id = ref(null); // getters const count = computed(() => todos.value.length); const findById = computed(() => todos.value.find(todo => todo.id === id.value)); const findIndex = computed(() => todos.value.findIndex(todo => todo.id === id.value)); // actions const create = (title, description) => { const created = {id: count.value, title, description}; todos.value = [created, ...todos.value]; } const update = (title, description) => { const index = findIndex(this.id); todos.value[index] = {...todos.value[index], title, description}; } const remove = id => { todos.value = todos.value.filter(todo => todo.id !== id); } return { todos, showForm, id, count, findById, create, update, remove } });
As you see i am using the vue 3 helpers for composition Api like ref(), computed(). So i refactored the state into reactive variables like todos, showForm, id. The getters become computed properties using the computed() helper. The actions become normal methods.
Then all the data i need to be consumed in components in the return statement as an object.
Now what’s left is to refactor the code in every component to use the setup store approach which i will this to you. Refer to Pinia docs in order to learn more and accomplish this.
You should use storeToRefs NOT a computed property to access the store, see https://stackoverflow.com/a/71677026
thanks