Provide, Inject, and Dynamic Components
Introduction
Props work well for parent → child, but passing data through many middle layers is prop drilling. provide / inject share values across the tree. Dynamic components swap which component renders with <component :is>. defineAsyncComponent loads components lazily. This chapter covers when each pattern fits.
Prerequisites
Prop Drilling Problem
App → Layout → Sidebar → Menu → MenuItem (needs theme)Passing theme through every layer clutters APIs. Provide/inject or Pinia solve this—Pinia for global app state; provide/inject for localized subtree context.
provide and inject
Parent provides:
Deep child injects:
Code explanation:
provide(key, value)in ancestorinject(key)in descendant—any depth- Use
readonlywhen children should not mutate directly
Injection Key with Symbol
Avoid string collisions:
import type { InjectionKey, Ref } from "vue";
export const themeKey: InjectionKey<Ref<"light" | "dark">> = Symbol("theme");
provide(themeKey, theme);
const theme = inject(themeKey);Default Value
const theme = inject("theme", ref("light"));Form Context Example
Wizard form sharing validation state:
Step components inject('wizard') and read/write formState.
For app-wide forms, Pinia may scale better.
Dynamic Components
Switch view by name:
Use shallowRef for component objects—avoid deep reactivity on component definitions.
String is with registered name (less common in SFC imports):
<component :is="'HomeView'" />Vue Router uses dynamic components internally—Vue Router.
defineAsyncComponent
Lazy load heavy panels:
Code explanation:
- Split code into separate chunk—loads on first render
Suspenseshows fallback while async component loads- Route-level lazy loading preferred for pages—Router chapter
Loading and error options:
const AsyncComp = defineAsyncComponent({
loader: () => import("./Heavy.vue"),
loadingComponent: LoadingSpinner,
errorComponent: LoadError,
delay: 200,
timeout: 10000,
});Dynamic + Transition
<Transition name="fade" mode="out-in">
<component :is="activeTab" :key="activeName" />
</Transition>:key forces transition when switching components.
When to Use What
| Pattern | Use |
|---|---|
| Props / emit | Direct parent-child |
| Slots | Custom markup in child |
| Provide / inject | Theme, i18n, form wizard context |
| Pinia | Global user, cart, settings |
| Dynamic component | Tabs, plugin panels |
| Async component | Large optional UI |
FAQ
provide/inject vs Pinia?
Provide for subtree scope; Pinia for app-wide shared state with devtools.
inject undefined?
Provider missing or wrong key—use defaults or throw in dev.
reactive provide?
Provide ref or reactive; inject readonly when children should emit events instead of mutating.
component string vs object?
Import object is tree-shaking friendly; string needs global registration.
Async component errors?
Use errorComponent or Router onError.
SSR with async component?
Requires Suspense / Nuxt—client-only wrap ClientOnly in Nuxt.
Same as React Context?
Similar to Context + dynamic React.lazy—Vue built-ins differ in API.