skip to content

In Vue 3, what does inject() return when no ancestor provides the key, and how do plain and factory defaults change that?

level: middleimportance: should knowfreq 40%

answer

  1. undefined, plus a warning
  2. the second argument silences it
  3. a function is a value unless told
  4. the third argument, true

basics

~20 s

With no provider and no default, inject() returns undefined and development builds warn that the injection was not found. A second argument supplies a default; with true as the third, a function default is called as a factory.

solid answer

~40 s

`inject(key)` with no matching ancestor (and no app-level provide) returns `undefined`, and in development Vue warns `injection "key" not found.` Passing a second argument, `inject('size', 'md')`, returns that default instead and suppresses the warning; any second argument does, even `null`, which is a clean way to mark an optional dependency. A function passed as the default is returned **as the value** unless the third argument is `true`: `inject('cache', () => new Map(), true)` calls the function only when no provider exists, which avoids building expensive or per-instance objects that would be thrown away. Choose a default when the component genuinely works alone; leave it off when a missing provider is a bug you want the warning for. With Symbol keys, the lookup rules are identical.

code

ts · 11 lines
ts
import { inject } from 'vue'

const size = inject('size', 'md')
const maybeForm = inject('form', null)
const cache = inject('cache', () => new Map<string, number>(), true)
const onSave = inject('onSave', () => console.log('saved'))

// size: provided value or 'md'
// maybeForm: provided value or null, never a warning
// cache: a new Map only when nothing is provided
// onSave: the arrow function itself when nothing is provided

go deeper

for a junior

Know that inject() returns undefined with a warning when nothing is provided, and that the second argument supplies a default.

for a middle

Explain the third-argument factory flag, why a function default is otherwise returned as-is, and that any second argument silences the warning.

for a senior

Decide per dependency whether a missing provider is a bug to surface or a standalone mode to support, and prefer Symbol keys in shared code.

for a principal

Make required versus optional injections an explicit part of each component's documented contract, especially in component libraries.

## What happens when nothing is provided `inject()` walks up from the component's parent to find the key, and finally checks the app-level provides. If it finds nothing: - the call returns `undefined`; - if no default was passed, a development build logs `injection "size" not found.` (with your key in place of `size`); - production builds return `undefined` silently. The Vue docs describe this as `inject` assuming by default that the key is provided somewhere in the parent chain, and warning when it is not. ## Plain defaults A second argument turns a required injection into an optional one: ```ts import { inject } from 'vue' const size = inject('size', 'md') // 'md' when no ancestor provides 'size' ``` Details that matter: 1. **Any second argument counts.** The check is whether a second argument was passed, so `inject('form', null)` returns `null` with no warning when there is no provider. That is a useful idiom for "works inside a form or alone". 2. **A provided `undefined` still wins.** If an ancestor provides the key with the value `undefined`, the key exists and the default is not used. 3. **Objects are shared.** `inject('options', {})` evaluates `{}` on every call anyway, but a default object you create once at module scope would be shared by every component that falls back to it. ## Factory defaults A function passed as the default is returned **as the value**, because a function is a legitimate thing to inject (a callback, a formatter). To have Vue call it instead, pass `true` as the third argument: ```ts // Returns the function itself when there is no provider const fmt = inject('formatter', (n: number) => n.toFixed(2)) // Calls the factory only when there is no provider const cache = inject('cache', () => new Map<string, unknown>(), true) ``` The factory form helps when the default is: - **expensive** to create, such as a large class instance, so it should not be built when a provider exists; - **per instance**, such as a `Map` each component should own; - **side-effecting** on creation, which should not happen needlessly. | Call | Provider exists | No provider | |---|---|---| | `inject('k')` | provided value | `undefined` + dev warning | | `inject('k', 'md')` | provided value | `'md'`, no warning | | `inject('k', null)` | provided value | `null`, no warning | | `inject('k', fn)` | provided value | `fn` itself | | `inject('k', fn, true)` | provided value | `fn()` result | ## Required or optional? Deciding whether to give a default is a design choice: - **Leave it off** when a missing provider is a programming error, such as a `FormField` used outside any `Form`. The warning makes the mistake visible in development. For a stronger signal, check the result and throw a clear error of your own. - **Give a default** when the component is meant to work standalone, such as a button that uses a provided size if there is one and `'md'` otherwise. - **Use a factory** when the fallback is costly or must be unique per component. ## A worked choice Consider a `Button` that can sit inside a `ButtonGroup`: - The group provides `size`, so buttons in it match. - A lone button must still work, so its injection has a plain default: `inject(sizeKey, 'md')`. - The group also provides a `register` function for keyboard navigation; a lone button has nothing to register with, so it uses `inject(groupKey, null)` and skips registration when the result is `null`. One component, two injections, two different fallback policies, each chosen for what a missing provider means. ## Keys The same rules apply to string and Symbol keys. The Vue docs recommend exported Symbol keys in larger apps and in component libraries, because two unrelated providers using the string `'size'` would silently shadow each other, while two `Symbol()` values never collide. ## Related APIs - `hasInjectionContext()` (Vue 3.3+) returns whether `inject()` can be called here without the wrong-place warning; libraries use it for helpers that may run inside or outside setup. - `app.runWithContext(fn)` (Vue 3.3+) runs `fn` with the app as the injection context, so `inject()` inside it can read app-level provides outside any component.

  • Why doesn't Vue simply call any function passed as an inject() default?
    Because a function is a valid injected value, for example a callback or formatter, and treating every function as a factory would make those impossible to default. The explicit `true` third argument says the function builds the default instead of being it.
  • A FormField must never be used outside a Form. Should its inject() have a default?
    No default, or a `null` default followed by an explicit check that throws a clear error. Without a default, development builds warn that the injection was not found; a thrown error with the component name is even more direct and also fires in production.

saying these in an interview costs you the question

  • inject() throws an error when no ancestor provides the key.
  • A function default is always called to produce the value.
  • Passing null as the default still triggers the not-found warning.
  • The default is used whenever the provided value is undefined.
  • String and Symbol keys follow different lookup rules.