Refresh#
Library authors frequently use provide/inject
API to transmit complex contextual information to their components and composition functions. Unlike props, which require explicit passing of dependencies by the consumer, dependency injection can seamlessly pass those dependencies without consumers even realizing it. This means that dependencies become invisible to consumers, effectively abstracting away unnecessary complexity.
import { provide, ref } from 'vue';
export function useAppUserContext() {
const user = ref(null);
onMounted(() => {
// Fetch the logged in user
user.value = await fetch('https://tes.t/me').json();
});
function logout() {
user.value = null;
}
function update(value) {
user.value = { ...user.value, ...value };
}
const ctx = {
user,
update,
logout
};
provide('AUTH_USER', ctx);
return ctx;
}
This isolated function will likely be called within the setup
function of your root app component. This enables all child components to effortlessly inject the user context and utilize it as needed. It's similar to stores in many ways, but also functions as a mechanism to empower users to create their own powerful iterations of them.
Use Injection Keys#
Declare a root injectionKeys.js file that contains all the injection keys you use in your app and prevent the usage of inline key values.
export const AUTH_USER_KEY = 'Authenticated User';
export const CART_KEY = 'User Cart';
Symbols are unique values that can be given a description and are always unique once created, they can be also used as object keys. This means we can use them for the injections keys here.
export const AUTH_USER_KEY = Symbol('USER');
// some injections later...
export const CURRENT_USER_KEY = Symbol('USER');
Use TypeScript#
Vue exposes a type called InjectionKey<TValue>
which magically lets provide/inject
infer and check the type of the values passed around. Here is a simple example:
// types.ts
interface UserCartContext {
items: CartItem[];
total: number;
}
// injectionKeys.ts
import { InjectionKey } from 'vue';
import { UserCartContext } from '@/types';
const CART_KEY: InjectionKey<UserCartContext> = Symbol(
'User Cart Context'
);
This will give you type checks at both the provide and inject levels, which is not something you should give up.
import { provide, inject, reactive } from 'vue';
import { CART_KEY } from '@/injectionKeys';
// ❌ Type Error
provide(CART_KEY, {});
const cart = reactive({ items: [], total: 0 });
// ✅
provide(CART_KEY, cart);
const cart = inject(CART_KEY);
// ❌ Type Error
cart?.map(...);
// ✅
cart?.items.map(...)
It is worth noting that the inject
function produces a resolved type in union with undefined
. This is because there is a possibility that the injection is not resolved. It is up to the user on how to handle this situation. To eliminate the undefined
, a fallback value has to be passed to the inject
function. The interesting thing here is that the fallback value also needs to pass the type checking.
import { inject } from 'vue';
import { ProductKey } from '@/symbols';
// ⛔️ Argument of type 'string' is not assignable to ...
const product = inject(ProductKey, 'nope');
// ✅ Type checks out
const product = inject(ProductKey, { name: '', price: 0 });
While you can provide plain value types, they are not that useful as usually you need to react to these values changing. You can create reactive injections as well with generic types.
For reactive refs created with ref
you can use the generic Ref
type to type your InjectionKey
, so it’s a nested generic type:
// types.ts
interface Product {
name: string;
price: number;
}
// symbols.ts
import { InjectionKey, Ref } from 'vue';
import { Product } from '@/types';
const ProductKey: InjectionKey<Ref<Product>> = Symbol('Product');
import { inject } from 'vue';
import { ProductKey } from '@/symbols';
const product = inject(ProductKey); // typed as Ref<Product> | undefined
product?.value; // typed as Product
Requiring Injections#
By default Vue displays a warning if it did not resolve an injection, Vue could’ve chosen to throw errors if an injection is not found but Vue can’t really make the assumption about whether the injection is required
or not, so it’s up to you to make sense of unresolved injection and the undefined
value.
function injectStrict<T>(key: InjectionKey<T>, fallback?: T) {
const resolved = inject(key, fallback);
if (!resolved) {
throw new Error(`Could not resolve ${key.description}`);
}
return resolved;
}
import { injectStrict } from '@/utils';
import { USER_CART_KEY } from '@/injectionKeys';
const cart = injectStrict(USER_CART_KEY);
// Here is safe to access `cart.items.map`
cart.items.map(...);
Injection Scopes#
import { getCurrentInstance, inject, InjectionKey } from 'vue';
export function injectWithSelf<T>(
key: InjectionKey<T>
): T | undefined {
const vm = getCurrentInstance() as any;
return vm?.provides[key as any] || inject(key);
}
What this function does is, before attempting to resolve the provided dependency with inject
. It attempts to locate it within the same component provided dependencies. Otherwise, it will follow the same behavior as inject
.
Singleton Injections#
import { provide } from 'vue';
import { CURRENT_USER_CTX } from '@/injectionKeys';
import { injectWithSelf } from '@/utils';
export function useAppUserContext() {
const existingContext = injectWithSelf(CURRENT_USER_CTX, null);
if (existingContext) {
return existingContext;
}
const user = ref(null);
onMounted(() => {
// Fetch the logged in user
user.value = await fetch('https://tes.t/me').json();
});
function logout() {
user.value = null;
}
function update(value) {
user.value = { ...user.value, ...value };
}
const ctx = {
user,
update,
logout
};
provide(CURRENT_USER_CTX, ctx);
return ctx;
}
By implementing this approach, irrespective of how many times and wherever you call useAppUserContext
, the code will only run once. Subsequently, all other calls will use the already injected instance. As a result, there is no need to use inject
in your components any longer. When you follow this pattern, you can use the context function to inject itself, which is much more intuitive and user-friendly than utilizing inject
and our customized versions directly. It should be noted that this procedure guarantees that an injection is created only once in a single component tree.
References:
https://logaretm.com/blog/making-the-most-out-of-vuejs-injections/
https://logaretm.com/blog/type-safe-provide-inject/