← All context files

vue3-options-api/state-and-routing.md

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 } }
  }
})