Nested routes, dynamic segments, lazy loading, beforeEach guards, and the navigation patterns that build a multi-page Vue SPA without full page reloads.
Nested routes, dynamic segments, lazy loading, beforeEach guards, and the navigation patterns that build a multi-page Vue SPA without full page reloads.
Imagine a shopping cart. The cart count shows in the navbar. The checkout button shows in the sidebar. The cart items list shows in the main view. All three components need the same data. You could pass it down through props, but that gets unwieldy fast (called 'prop drilling').
A store is a single place where shared state lives. Any component can read from it or write to it directly. Pinia is the official Vue store β it's lightweight, TypeScript-friendly, and integrates with Vue DevTools.
npm install pinia
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia()) // register Pinia with your app
app.mount('#app')
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// defineStore takes an ID (must be unique) and a setup function
export const useCartStore = defineStore('cart', () => {
// state: reactive variables
const items = ref([])
// getters: computed properties
const totalItems = computed(() =>
items.value.reduce((sum, item) => sum + item.qty, 0)
)
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
)
// actions: functions that modify state
function addItem(product) {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.qty++
} else {
items.value.push({ ...product, qty: 1 })
}
}
function removeItem(productId) {
items.value = items.value.filter(i => i.id !== productId)
}
function clearCart() {
items.value = []
}
// Return everything the component can access
return { items, totalItems, totalPrice, addItem, removeItem, clearCart }
})
{{ product.name }}
${{ product.price }}
useCartStore() in multiple components and they all share the exact same instance. Change it in one component and every other component that uses it updates automatically. That's the whole point.Pinia actions can be async β just add async to the function. This is where you'd put API calls.
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('users', () => {
const users = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchUsers() {
loading.value = true
error.value = null
try {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
users.value = await res.json()
} catch (e) {
error.value = 'Failed to load users'
} finally {
loading.value = false
}
}
return { users, loading, error, fetchUsers }
})
Loading...
{{ store.error }}
- {{ user.name }}
ProductList.vue component with 6 hardcoded products and an 'Add to Cart' button on each.CartSidebar.vue that shows all items in the cart with quantity and price.useYourStore() in any component. They all share the same reactive instance.<script setup>.The foundations from today carry directly into Day 4. In the next session the focus shifts to Pinia State Management β building directly on everything covered here.
Before moving on, verify you can answer these without looking:
Live Bootcamp
Learn this in person β 2 days, 5 cities
ThuβFri sessions in Denver, Los Angeles, New York, Chicago, and Dallas. $1,490 per seat. JuneβOctober 2026.
Reserve Your Seat →