State Management
Learn how to manage state in Vue.js applications using Pinia, the official state management solution. State management helps you share data between components and keep your application organized.
What is State Management?
State management is the practice of storing and managing application data in a centralized location. Instead of passing data between components through props and events, you store it in a “store” that any component can access.
Why Use Pinia?
Pinia is the recommended state management solution for Vue.js because:
- Simple and intuitive - Easy to learn and use
- TypeScript friendly - Excellent TypeScript support
- Devtools integration - Great debugging experience
- Modular by design - Organize stores by feature
- No mutations - Simpler than Vuex
Setting Up Pinia
Installation
First, install Pinia in your project:
npm install piniaConfiguration
Set up Pinia in your main application file:
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')Creating Your First Store
A store is where you define your state, getters, and actions for a specific feature.
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter App'
}),
getters: {
doubleCount: (state) => state.count * 2,
isEven: (state) => state.count % 2 === 0
},
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
reset() {
this.count = 0
},
incrementBy(amount) {
this.count += amount
}
}
})Using Stores in Components
Basic Usage
<template>
<div class="counter-display">
<h2>{{ counterStore.name }}</h2>
<p>Count: {{ counterStore.count }}</p>
<p>Double Count: {{ counterStore.doubleCount }}</p>
<p>Is Even: {{ counterStore.isEven ? 'Yes' : 'No' }}</p>
<button @click="counterStore.increment()">+</button>
<button @click="counterStore.decrement()">-</button>
<button @click="counterStore.reset()">Reset</button>
<button @click="counterStore.incrementBy(5)">+5</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
<style scoped>
.counter-display {
text-align: center;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
button {
margin: 0 5px;
padding: 8px 16px;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #369870;
}
</style>Options API Usage
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
</div>
</template>
<script>
import { useCounterStore } from '@/stores/counter'
import { mapState, mapActions } from 'pinia'
export default {
computed: {
...mapState(useCounterStore, ['count', 'doubleCount'])
},
methods: {
...mapActions(useCounterStore, ['increment', 'decrement'])
}
}
</script>Real-World Example: User Store
Here’s a more practical example for managing user authentication:
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// State
const user = ref(null)
const isLoading = ref(false)
const error = ref(null)
// Getters
const isAuthenticated = computed(() => !!user.value)
const userName = computed(() => user.value?.name || 'Guest')
const userRole = computed(() => user.value?.role || 'user')
// Actions
const login = async (credentials) => {
isLoading.value = true
error.value = null
try {
// Simulate API call
const response = await mockApiLogin(credentials)
user.value = response.user
return response
} catch (err) {
error.value = err.message
throw err
} finally {
isLoading.value = false
}
}
const logout = () => {
user.value = null
error.value = null
}
const updateProfile = async (profileData) => {
isLoading.value = true
try {
// Simulate API call
const response = await mockApiUpdateProfile(profileData)
user.value = { ...user.value, ...response.user }
return response
} catch (err) {
error.value = err.message
throw err
} finally {
isLoading.value = false
}
}
return {
// State
user,
isLoading,
error,
// Getters
isAuthenticated,
userName,
userRole,
// Actions
login,
logout,
updateProfile
}
})
// Mock API functions (replace with real API calls)
async function mockApiLogin(credentials) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (credentials.email === 'user@example.com' && credentials.password === 'password') {
resolve({
user: {
id: 1,
name: 'John Doe',
email: 'user@example.com',
role: 'user'
},
token: 'mock-jwt-token'
})
} else {
reject(new Error('Invalid credentials'))
}
}, 1000)
})
}
async function mockApiUpdateProfile(profileData) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ user: profileData })
}, 500)
})
}Using the User Store
<!-- components/LoginForm.vue -->
<template>
<div class="login-form">
<h2>Login</h2>
<form @submit.prevent="handleLogin">
<div class="form-group">
<label for="email">Email:</label>
<input
id="email"
v-model="email"
type="email"
required
/>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input
id="password"
v-model="password"
type="password"
required
/>
</div>
<button type="submit" :disabled="userStore.isLoading">
{{ userStore.isLoading ? 'Logging in...' : 'Login' }}
</button>
<div v-if="userStore.error" class="error">
{{ userStore.error }}
</div>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const email = ref('')
const password = ref('')
const handleLogin = async () => {
try {
await userStore.login({
email: email.value,
password: password.value
})
// Redirect or show success message
} catch (error) {
// Error is already handled in the store
}
}
</script>
<style scoped>
.login-form {
max-width: 300px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
width: 100%;
padding: 10px;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #ccc;
}
.error {
margin-top: 10px;
color: red;
text-align: center;
}
</style>Store Organization
For larger applications, organize your stores by feature:
stores/
├── auth.js # Authentication state
├── user.js # User profile state
├── products.js # Product catalog state
├── cart.js # Shopping cart state
├── notifications.js # Notification state
└── index.js # Store exports (optional)Best Practices
- Keep stores focused - Each store should manage one domain
- Use composition stores - They’re more flexible and easier to test
- Don’t overuse stores - Only use stores for truly shared state
- Handle side effects in actions - Keep API calls and async operations in actions
- Use TypeScript - Get better type safety and autocompletion
Next Steps
Learn how to integrate with APIs in the API integration tutorial.