Vue 3 Composition API: A Practical Introduction
setup(), ref vs reactive, and reusable composables — a practical introduction to Vue 3's Composition API for developers coming from Options API or another framework.
Vuex was Vue's original state management library, but Pinia has replaced it as the officially recommended choice — simpler API, full TypeScript support, and no more mutation boilerplate. Here's how to actually use it.
Vuex required a strict state/getters/mutations/actions split, and mutations existed purely to satisfy Vue DevTools — you couldn't change state directly even when it would've been perfectly safe. Pinia drops mutations entirely: actions can modify state directly, and it still fully supports time-travel debugging.
npm install pinia
// stores/counter.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubled: (state) => state.count * 2,
},
actions: {
increment() {
this.count++; // direct mutation — no separate mutation function needed
},
},
});
<script setup>
import { useCounterStore } from '@/stores/counter';
const counter = useCounterStore();
</script>
<template>
<button @click="counter.increment()">{{ counter.count }} ({{ counter.doubled }})</button>
</template>
No mapState/mapActions helpers needed — the store's properties and actions are just plain reactive object properties, destructurable with storeToRefs when you need to keep reactivity after destructuring.
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() { count.value++; }
return { count, doubled, increment };
});
This mirrors a component's own <script setup> structure exactly — if Composition API syntax already feels natural, this store style will too.
Pinia has no built-in persistence, but the community pinia-plugin-persistedstate package handles localStorage syncing in one line of config — essential for things like a shopping cart that should survive a page refresh.
Not every piece of state belongs in Pinia. Component-local UI state (a form's current input, a dropdown's open/closed state) should stay in that component. Reach for a store specifically for state that's genuinely shared across multiple, unrelated parts of the app — auth status, a shopping cart, user preferences.
setup(), ref vs reactive, and reusable composables — a practical introduction to Vue 3's Composition API for developers coming from Options API or another framework.
How Inertia.js lets a Laravel backend and Vue.js frontend feel like one SPA-style codebase, without building a separate JSON API.