In a Vue 3 app, how do provide() and inject() get a layout's theme to deeply nested components without passing props?
answer
- a key and a value
- only the provider's descendants
- called synchronously in setup
- app-wide with app.provide
basics
~20 sThe layout calls provide('theme', theme) in its setup; any descendant, however deep, calls inject('theme') in its setup to receive it. Components in between pass nothing, and only the provider's subtree can inject it; app.provide() makes a value available app-wide.
solid answer
~40 sIn the layout's `<script setup>`, `provide('theme', theme)` registers a value under a key (a string or a Symbol). Any descendant component, at any depth, calls `inject('theme')` during its own setup and gets that value; the components in between neither declare a prop nor forward anything. Only the provider's descendants can inject it, and if several ancestors provide the same key, the nearest one wins. Both calls must run synchronously during `setup()`. For values every component should see, such as the app's locale or a plugin's service, `app.provide(key, value)` on the application instance provides at app level. Props are still the better choice for direct children and for anything that is part of a component's public contract; provide/inject fits cross-cutting values that many descendants need.
code
vue · 11 lines<script setup lang="ts">
import { ref, provide } from 'vue'
import { themeKey } from './keys'
const theme = ref<'light' | 'dark'>('dark')
provide(themeKey, theme)
</script>
<template>
<main :data-theme="theme"><slot /></main>
</template>go deeper
Recall provide(key, value) in an ancestor's setup and inject(key) in a descendant's setup, and that the components in between do nothing.
Explain the lookup rules: descendants only, nearest provider wins, a component never injects its own provide, and the synchronous setup requirement.
Choose between props, provide/inject and app.provide by how visible the dependency should be, and use Symbol keys for anything shared across a large codebase.
Set limits on what is provided ambiently so hidden dependencies stay few, documented and owned by the component that provides them.
## The problem: prop drilling A layout knows the current **theme** (light or dark, plus a few tokens). A button three levels down needs it. With props alone, every component in between must declare a `theme` prop and pass it on, even though none of them use it. This is **prop drilling**: noisy, fragile, and it couples components that should not know about the theme. ## The two calls Vue 3's dependency injection has two functions, both imported from `vue`: - `provide(key, value)`: called in an ancestor, registers `value` under `key` for its **descendants**. - `inject(key)` / `inject(key, defaultValue)`: called in a descendant, returns the value provided by the **nearest ancestor** with that key. ```vue <!-- AppLayout.vue --> <script setup> import { ref, provide } from 'vue' const theme = ref('dark') provide('theme', theme) </script> <template> <header>...</header> <slot /> </template> ``` ```vue <!-- ThemedButton.vue, several levels below --> <script setup> import { inject } from 'vue' const theme = inject('theme') </script> <template> <button :class="`btn-${theme}`"><slot /></button> </template> ``` Nothing between `AppLayout` and `ThemedButton` mentions the theme. ## Rules Vue enforces 1. **Synchronous in setup.** Both `provide()` and `inject()` must be called synchronously during a component's `setup()` (or `<script setup>`), like lifecycle hooks. Called later, `provide()` warns `provide() can only be used inside setup().` and `inject()` outside any component context warns `inject() can only be used inside setup() or functional components.` 2. **Descendants only.** A provided value is visible to the provider's subtree, not to siblings or ancestors. 3. **Nearest provider wins.** A nested layout that provides `'theme'` again overrides the outer one for its own subtree. 4. **Not to itself.** A component that provides a key and injects the same key receives its **parent's** value, because `inject()` starts its lookup at the parent. ## How the lookup works inside Vue Vue's runtime keeps a `provides` object on every component instance. By default an instance simply reuses its parent's object. The first time a component calls `provide()`, Vue gives it its own `provides` object created with `Object.create(parentProvides)`, so the parent's keys are reachable through the prototype chain: - a descendant's `inject()` reads the key from its parent's `provides`, and the prototype chain finds the nearest ancestor that set it; - a root component falls back to the app's provides, which is where `app.provide()` values live; - components that provide nothing add no cost; lookups are plain property reads. This is also why a component cannot inject its own value: its lookup starts at the parent's object, not its own. ## Keys: strings or Symbols String keys are easy to read but can collide in a large app or in a component library. The Vue docs recommend exporting **Symbol** keys from a dedicated file (`export const themeKey = Symbol()`) when many providers exist or when other developers consume your components. ## App-level provide `provide()` needs a component. For values the whole app should see, the application instance has its own: ```ts import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.provide('locale', 'en') app.mount('#app') ``` App-level provides are available to **every** component in that app, and they are how plugins make services available, since a plugin has no component in which to call `provide()`. | | `provide()` in a component | `app.provide()` | |---|---|---| | Who can inject | that component's descendants | every component in the app | | Where it is called | synchronously in `setup()` | on the app instance, before or after mount | | Typical use | a theme per layout, a form context | app-wide locale, plugin services | | Can be overridden deeper | by a nearer provider | by any component providing the same key | ## When to use props instead provide/inject hides a dependency: reading a component's props no longer tells you everything it needs. So: - Use **props** for direct parent-to-child data and for anything that is part of a reusable component's public API. - Use **provide/inject** for cross-cutting values many descendants need (theme, locale, a form's context) where drilling would touch components that do not care. - Keep provided values few and well named, and document which keys a component expects.
- A Vue component calls provide('theme', 'dark') and then inject('theme') itself. What does it get?Its parent's value for `'theme'` (or the default, or a missing-injection warning), not `'dark'`. `inject()` starts looking at the parent's provides, so a component never injects what it provides itself. Its own value is only visible to its descendants.
- Can a Vue component call inject() inside a click handler or a setTimeout callback?No. `inject()`, like `provide()`, must be called synchronously during setup. In a handler or timer there is no current component to resolve against, so Vue warns and returns undefined. Inject at the top of setup, keep the result in a variable, and use it later.
- When would you still pass the theme as a prop?When the consumer is a direct child, or when the theme is part of a reusable component's public API. Props make the dependency visible in the component's contract and in the parent's template; injection hides it, which is worth it only when several layers would otherwise forward it.
provide is like a Wi-Fi network set up in one wing of a building: every room in that wing can join it by name without cables through each hallway, and rooms outside the wing cannot see it.
saying these in an interview costs you the question
- Every intermediate component must declare the injected key as a prop.
- Sibling components can inject a value their sibling provides.
- inject() can be called anywhere, for example inside a click handler.
- A component can inject the value it provides itself.
- provide/inject should replace props for all parent-child data.