
Michael Hoffmann
@mokkapps

State management in Vue is one of those topics that seems simple until your app grows a little - and then you’re suddenly juggling prop drilling, duplicated logic, and confusing reactivity bugs. Over the years I’ve tried different approaches and learned that the right answer usually isn’t "one tool to rule them all" but choosing the right approach for the problem at hand. In this article I’ll walk you through composables, provide/inject, and Pinia — when to use each, how to use them well, and practical examples you can copy into your projects.
When I start a new Vue project I always ask three questions before picking a state approach:
Vue gives us multiple, complementary tools to solve these needs: Composables (Composition API logic encapsulation), Provide/Inject (scoped sharing in a subtree), and Pinia (centralized/global stores). Each has strengths and trade-offs. Below I’ll dig into each with patterns, code, and rules of thumb I actually use day-to-day.
Composables are reusable functions that encapsulate state and logic using the Composition API. Think of them as the “utility modules” for stateful behavior.
Why I reach for a composable:
A minimal counter composable example:
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => ++count.value
const decrement = () => --count.value
const reset = () => {
count.value = initial
}
return { count, increment, decrement, reset }
}
Best practices I follow:
useAuth, useFetch, useCounter. This makes intent clear./composables/auth, /composables/ui).refs unnecessarily).When not to use a composable:
Provide/Inject lets a parent component provide values (reactive data, functions) and descendant components inject them without prop drilling. I use this pattern when state belongs to a component subtree - e.g., a theming context, a form with nested children, or a modal manager.
Example: simple theming using Provide/Inject
<script setup lang="ts">
import { provide, reactive } from 'vue'
const theme = reactive({ color: 'light', accent: '#009688' })
const toggle = () => theme.color = theme.color === 'light' ? 'dark' : 'light'
provide('theme', theme)
provide('toggleTheme', toggle)
</script>
<script setup lang="ts">
import { inject } from 'vue'
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
</script>
When to use Provide/Inject:
Caveats and best practices:
const modal = useModal(); provide('modal', modal)) so you get the composable API and scoped sharing together.Pinia is the officially recommended state library for Vue 3. I use Pinia for global state that multiple, unrelated components or pages need access to - authentication, user preferences, shopping cart, complex domain models.
Key reasons to choose Pinia:
Example: simple auth store with Pinia
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const token = ref(null)
const isLoggedIn = computed(() => !!user.value)
function setUser(payload) {
user.value = payload.user
token.value = payload.token
}
function logout() {
user.value = null
token.value = null
}
return { user, token, isLoggedIn, setUser, logout }
})
Usage in a component:
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth'
import { storeToRefs } from 'pinia'
const auth = useAuthStore()
const { isLoggedIn } = storeToRefs(auth)
</script>
Pinia best practices I follow:
authStore, cartStore, uiStore) instead of one giant store.I like to compare the three on the dimensions that matter most in real projects:
Rules of thumb I use:
State management in Vue doesn’t have to be either/or. Composables, Provide/Inject, and Pinia are complementary tools - choose based on scope, reusability, and complexity.
My checklist before implementing state:
