Vue 3 Options API – Pinia and Vue Router
Pinia (state management)
Pinia replaces Vuex in Vue 3. With Options API, you typically use the option store style (which mirrors a component).
Defining a store
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
profile: null,
loading: false,
}),
getters: {
isLoggedIn: (state) => !!state.profile,
displayName: (state) => state.profile?.name ?? 'Guest',
},
actions: {
async load() {
this.loading = true
try {
this.profile = await api.get('/me')
} finally {
this.loading = false
}
},
logout() {
this.profile = null
},
},
})
Using a store from an Options API component
The cleanest pattern is mapState / mapActions from Pinia:
<script>
import { mapState, mapActions } from 'pinia'
import { useUserStore } from '@/stores/user'
export default {
computed: {
// expose state + getters
...mapState(useUserStore, ['profile', 'loading', 'isLoggedIn', 'displayName']),
},
methods: {
// expose actions
...mapActions(useUserStore, ['load', 'logout']),
},
async mounted() {
if (!this.profile) await this.load()
},
}
</script>
Direct access (when you need the store instance)
import { useUserStore } from '@/stores/user'
export default {
computed: {
user() { return useUserStore() }
},
methods: {
doThing() {
this.user.profile = { /* ... */ } // Pinia state is mutable directly
},
},
}
Setup at app boot
// 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')
Vue Router
Defining routes
// router.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', name: 'home', component: () => import('@/views/Home.vue') },
{ path: '/users/:id', name: 'user', component: () => import('@/views/User.vue'), props: true },
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFound.vue') },
]
export default createRouter({
history: createWebHistory(),
routes,
})
props: true
Maps route params to component props — much cleaner than this.$route.params.
<!-- User.vue -->
<script>
export default {
props: {
id: { type: String, required: true },
},
async mounted() {
this.user = await api.get(`/users/${this.id}`)
},
}
</script>
Programmatic navigation
methods: {
goHome() {
this.$router.push({ name: 'home' })
},
goToUser(id) {
this.$router.push({ name: 'user', params: { id } })
},
goWithQuery() {
this.$router.push({ path: '/search', query: { q: this.term } })
},
}
Watch a route param
When id changes from /users/1 to /users/2, the component is reused — you
need to watch:
watch: {
id: {
handler(newId) { this.fetch(newId) },
immediate: true,
},
}
Or use beforeRouteUpdate:
beforeRouteUpdate(to, from, next) {
this.fetch(to.params.id)
next()
}
Route guards on the component
export default {
beforeRouteEnter(to, from, next) {
// `this` is NOT available here — component not constructed yet
next((vm) => vm.fetch(to.params.id))
},
beforeRouteLeave(to, from, next) {
if (this.dirty && !confirm('Discard changes?')) return next(false)
next()
},
}
Global guards (auth)
router.beforeEach(async (to) => {
const user = useUserStore()
if (to.meta.requiresAuth && !user.isLoggedIn) {
return { name: 'login', query: { next: to.fullPath } }
}
})