Share state across components the right way — lift state to a common parent, two-way bind with defineModel, provide/inject for app-wide values, and Pinia stores.
Why: when two siblings need the same data, the state moves to their closest common parent — one owns it, both receive it. The child gets the value as a prop and reports changes up with an emit. This is the first tool to reach for; most "state management" problems dissolve right here.
<!-- app/components/FilterBox.vue — reports changes up via an emit -->
<script setup lang="ts">
defineProps<{ value: string }>()
const emit = defineEmits<{ change: [value: string] }>()
</script>
<template>
<input
:value="value"
@input="emit('change', ($event.target as HTMLInputElement).value)"
/>
</template>
<!-- app/pages/index.vue — both siblings need query, so the parent owns it -->
<script setup lang="ts">
import { ref } from 'vue'
const query = ref('')
</script>
<template>
<FilterBox :value="query" @change="query = $event" />
<p>Results for: {{ query || '(everything)' }}</p>
</template>Why: the prop-down/emit-up dance from the last topic is so common that Vue gives it one line: defineModel creates a prop the child can write back to the parent, and the parent connects with plain v-model — exactly like binding an input.
<!-- app/components/SearchInput.vue -->
<script setup lang="ts">
// defineModel = a prop the child may update — sugar over :value + @input
const model = defineModel<string>({ default: '' })
</script>
<template>
<input v-model="model" placeholder="Search…" />
</template>
<!-- app/pages/index.vue — v-model just works on your component -->
<script setup lang="ts">
import { ref } from 'vue'
const query = ref('')
</script>
<template>
<SearchInput v-model="query" />
<p>Searching for: {{ query }}</p>
</template>Why: provide/inject delivers a value to any component below the provider without threading it through every layer of props — Vue's version of React context. When: values the whole tree needs but that rarely change — the signed-in user, theme, locale.
<!-- app/app.vue — provide once near the top of the tree -->
<script setup lang="ts">
import { provide, ref } from 'vue'
const theme = ref<'light' | 'dark'>('dark')
provide('theme', theme)
</script>
<template>
<NuxtPage />
</template>
<!-- Any component below, no matter how deep -->
<script setup lang="ts">
import { inject, type Ref } from 'vue'
const theme = inject<Ref<'light' | 'dark'>>('theme')!
</script>
<template>
<p>Current theme: {{ theme }}</p>
<button @click="theme = theme === 'dark' ? 'light' : 'dark'">
Toggle theme
</button>
</template>Why: for client state that changes often and is read in many places — carts, filters, UI panels — Pinia is the official store library: a defineStore call holds the state, the derived values, and the functions that change them, and any component can use it without props or provide/inject. Note: in Nuxt, install with (pnpm add pinia @pinia/nuxt) or (npm install pinia @pinia/nuxt) and add @pinia/nuxt to the modules list in nuxt.config.ts.
// stores/cart.ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
// A store looks just like a composable: refs + computed + functions
export const useCartStore = defineStore('cart', () => {
const items = ref<string[]>([])
const count = computed(() => items.value.length)
function add(item: string) {
items.value.push(item)
}
function clear() {
items.value = []
}
return { items, count, add, clear }
})
// In any component:
// const cart = useCartStore()
// <button @click="cart.add('Book')">Add book</button>
// <span>Cart: {{ cart.count }}</span>