DEV Community

Cover image for Mastering State Management in Vue.js with Pinia
Delia
Delia

Posted on

Mastering State Management in Vue.js with Pinia

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
Enter fullscreen mode Exit fullscreen mode

Next, install Pinia:

npm install pinia
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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++
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
    },
  },
})
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode

Persisting State

To persist the state across page reloads, you can use plugins like pinia-plugin-persistedstate:

npm install pinia-plugin-persistedstate
Enter fullscreen mode Exit fullscreen mode
// 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')
Enter fullscreen mode Exit fullscreen mode
// 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,
})
Enter fullscreen mode Exit fullscreen mode

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)
      }
    },
  },
})
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
shadyarbzharothman profile image
Shadyar Bzhar Othman

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?

Collapse
 
delia_code profile image
Delia

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.