Vue cookbook · Intermediate
Share state down the tree with typed provide/inject
Deeply nested components need theme or form context without prop drilling.
Approach
Create an InjectionKey<T> so provide and inject are typed, provide readonly state plus functions to change
it, and throw a clear error when a component is used outside its provider.
Solution
// tabs.ts
import { inject, provide, readonly, ref, type InjectionKey, type Ref } from "vue";
type TabsContext = { active: Readonly<Ref<string>>; select: (id: string) => void };
const TabsKey: InjectionKey<TabsContext> = Symbol("tabs");
export function provideTabs(initial: string) {
const active = ref(initial);
const context: TabsContext = { active: readonly(active), select: (id) => (active.value = id) };
provide(TabsKey, context);
return context;
}
export function useTabs() {
const context = inject(TabsKey);
if (!context) throw new Error("useTabs() must be used inside <Tabs>");
return context;
}
<!-- Tab.vue -->
<script setup lang="ts">
import { computed } from "vue";
import { useTabs } from "./tabs";
const props = defineProps<{ id: string; label: string }>();
const { active, select } = useTabs();
const selected = computed(() => active.value === props.id);
</script>
<template>
<button role="tab" :aria-selected="selected" @click="select(id)">{{ label }}</button>
</template>
Pitfalls
- Providing a writable ref that any descendant can change; expose readonly state and functions.
- String keys that collide between libraries; use
Symbol-basedInjectionKeys. - Using provide/inject for app-wide state better kept in Pinia.
Variations
app.provide(key, value)provides values to the whole app (plugins).- Pair with
defineSlotsand compound components for headless UI libraries.
Further reading: Provide / Inject
Reference: https://vuejs.org/guide/components/provide-inject.html
More Vue recipes
- Access child component methods with template refs and defineExpose
- Add SEO metadata and structured data in Nuxt
- Build an accessible modal with Teleport
- Build custom inputs with defineModel and modifiers
- Catch component errors with an error boundary
- Debounce user input with a custom ref
- Fetch data in a composable and cancel stale requests
- Fetch, refresh and share data with Nuxt useFetch