State management in front-end applications is crucial as your app scales and grows in complexity. If you’re familiar with Vue.js, you might have heard of Vuex, the official state management library. But have you heard of Pinia? Pinia is a lightweight alternative to Vuex, providing an intuitive API, full TypeScript support, and a modular design. In this tutorial, we’ll dive into state management using Pinia, starting from the basics and moving to more advanced concepts.
Introduction to Pinia
What is Pinia?
Pinia is a state management library for Vue.js that was inspired by Vuex but designed to be more intuitive and less boilerplate-heavy. It leverages the latest Vue.js composition API, making it a modern and efficient tool for managing state in your Vue applications.
Why Use Pinia?
- Simplicity: Pinia's API is simple and easy to use.
- Modularity: Encourages modular store definitions.
- TypeScript Support: Built with TypeScript support in mind.
- DevTools Integration: Pinia integrates seamlessly with Vue DevTools.
Getting Started with Pinia
Installation
First, let's set up a Vue.js project with Pinia. If you don't have a Vue project yet, you can create one using Vue CLI:
npm install -g @vue/cli
vue create my-vue-app
cd my-vue-app
Next, install Pinia:
npm install pinia
Setting Up Pinia
To use Pinia in your Vue application, you need to create a Pinia instance and pass it to your Vue app:
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
Creating a Store
Stores in Pinia are similar to Vuex but are defined using functions. Let’s create a basic store:
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
In the code above:
- state: Returns an object with the initial state.
- getters: Compute derived state based on the current state.
- actions: Methods that can change the state and contain business logic.
Using the Store in Components
Now, let’s use this store in a Vue component:
<!-- Counter.vue -->
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const counterStore = useCounterStore()
return {
count: counterStore.count,
doubleCount: counterStore.doubleCount,
increment: counterStore.increment,
}
},
}
</script>
Advanced Concepts with Pinia
Modular Stores
As your application grows, it’s essential to keep your stores modular. You can create multiple stores and use them together:
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: 'John Doe',
isLoggedIn: false,
}),
actions: {
login(name) {
this.name = name
this.isLoggedIn = true
},
logout() {
this.name = ''
this.isLoggedIn = false
},
},
})
<!-- User.vue -->
<template>
<div>
<p v-if="isLoggedIn">Welcome, {{ name }}</p>
<button v-if="!isLoggedIn" @click="login('Jane Doe')">Login</button>
<button v-if="isLoggedIn" @click="logout">Logout</button>
</div>
</template>
<script>
import { useUserStore } from '@/stores/user'
export default {
setup() {
const userStore = useUserStore()
return {
name: userStore.name,
isLoggedIn: userStore.isLoggedIn,
login: userStore.login,
logout: userStore.logout,
}
},
}
</script>
Persisting State
To persist the state across page reloads, you can use plugins like pinia-plugin-persistedstate
:
npm install pinia-plugin-persistedstate
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persistedstate'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPersist)
app.use(pinia)
app.mount('#app')
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
persist: true,
})
Handling Asynchronous Actions
Pinia allows you to handle asynchronous operations within your actions. Here’s an example of fetching data from an API:
// stores/posts.js
import { defineStore } from 'pinia'
import axios from 'axios'
export const usePostStore = defineStore('post', {
state: () => ({
posts: [],
}),
actions: {
async fetchPosts() {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts')
this.posts = response.data
} catch (error) {
console.error('Failed to fetch posts:', error)
}
},
},
})
<!-- Posts.vue -->
<template>
<div>
<button @click="fetchPosts">Fetch Posts</button>
<ul>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</div>
</template>
<script>
import { usePostStore } from '@/stores/posts'
export default {
setup() {
const postStore = usePostStore()
return {
posts: postStore.posts,
fetchPosts: postStore.fetchPosts,
}
},
}
</script>
Conclusion
Pinia is a robust and efficient state management solution for Vue.js applications, offering simplicity, modularity, and excellent TypeScript support. By following this tutorial, you should now have a solid understanding of how to get started with Pinia, manage state, and handle more advanced use cases like modular stores, state persistence, and asynchronous actions.
Embrace Pinia to make your Vue.js applications more maintainable and scalable. Share your journey and progress with the hashtag #PiniaJourney to inspire others in the community!
Happy coding! 🚀
Top comments (2)
Thanks, what's the best way to handle api response in pinia and axios? caching the data or just storing it in a state of the store and when an action like delete happen just delete it from the state and not fetch the data again?
What's the best way to handle this?
I would store the data in a state , but re-fetch it after a CRUD operation. Just deleting it from the state leaves room for errors and inconsistent data. It is just a personal opinion because both options work but from my experience detecting it in the state sometimes creates data inconsistency.