Vue cookbook · Intermediate

Share state down the tree with typed provide/inject

Deeply nested components need theme or form context without prop drilling.

Create a free account to save this page, take notes and track your progress. Sign in

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-based InjectionKeys.
  • 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 defineSlots and compound components for headless UI libraries.

Further reading: Provide / Inject

Reference: https://vuejs.org/guide/components/provide-inject.html

More Vue recipes

Was this page helpful?