En esta página
provide() e inject() — inyección de dependencias
¿Qué problema resuelve provide/inject?
Imagina que tienes una jerarquía de componentes donde el componente raíz tiene datos que necesita un componente nieto (o bisnieto):
App
└── Layout
└── Sidebar
└── PerfilUsuario ← necesita datos del usuarioSin provide/inject, tendrías que pasar los datos como props a través de cada nivel intermedio aunque esos niveles no los necesiten. Esto se llama prop drilling y hace el código difícil de mantener.
provide() permite que un ancestro haga disponibles datos a cualquier descendiente, sin importar cuántos niveles de profundidad, sin necesidad de pasar props por cada nivel intermedio.
provide() — proporcionar datos
provide() se llama en el componente ancestro. Toma una clave (string o Symbol) y un valor:
<script setup lang="ts">
import { ref, provide } from 'vue'
// Con string como clave (menos seguro para TypeScript)
provide('titulo', ref('Mi Aplicación'))
// Con Symbol (recomendado)
const TITULO_KEY = Symbol('titulo')
provide(TITULO_KEY, ref('Mi Aplicación'))
</script>El valor puede ser cualquier cosa: primitivos, refs, reactive objects, funciones, o una combinación en un objeto.
inject() — consumir datos
inject() se llama en cualquier componente descendiente:
<script setup lang="ts">
import { inject } from 'vue'
// Puede devolver undefined si no hay provide en ningún ancestro
const titulo = inject('titulo')
// Con valor por defecto — nunca será undefined
const titulo2 = inject('titulo', ref('Título por defecto'))
</script>InjectionKey — tipado seguro
El problema de usar strings como claves es que TypeScript no sabe qué tipo devuelve inject(). La solución es InjectionKey<T>:
// types/injection-keys.ts
import { type InjectionKey, type Ref } from 'vue'
export const CONFIG_APP: InjectionKey<{
apiUrl: string
version: string
debug: Ref<boolean>
}> = Symbol('config-app')<!-- Provider -->
<script setup lang="ts">
import { ref, provide } from 'vue'
import { CONFIG_APP } from '@/types/injection-keys'
provide(CONFIG_APP, {
apiUrl: 'https://api.ejemplo.com',
version: '1.0.0',
debug: ref(false),
})
</script><!-- Consumer — TypeScript sabe el tipo exacto -->
<script setup lang="ts">
import { inject } from 'vue'
import { CONFIG_APP } from '@/types/injection-keys'
const config = inject(CONFIG_APP)
// config es { apiUrl: string; version: string; debug: Ref<boolean> } | undefined
if (!config) throw new Error('CONFIG_APP no proporcionado')
config.debug.value = true // TypeScript reconoce que es Ref<boolean>
</script>provide/inject a nivel de aplicación
Puedes proporcionar valores a toda la aplicación desde main.ts:
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { ANALYTICS_KEY } from './types/injection-keys'
const app = createApp(App)
// Disponible en TODOS los componentes
app.provide(ANALYTICS_KEY, {
track: (evento: string) => console.log('Track:', evento),
})
app.mount('#app')Esto es útil para servicios globales como analytics, sistema de notificaciones, o configuración de la app.
Proporcionar datos reactivos
Para que los componentes que inyectan vean los cambios, el valor proporcionado debe ser reactivo:
<script setup lang="ts">
import { ref, readonly, provide } from 'vue'
import { CARRITO_KEY } from '@/types/injection-keys'
const items = ref<{ id: number; nombre: string; cantidad: number }[]>([])
function agregarItem(item: { id: number; nombre: string }) {
const existente = items.value.find(i => i.id === item.id)
if (existente) {
existente.cantidad++
} else {
items.value.push({ ...item, cantidad: 1 })
}
}
function eliminarItem(id: number) {
items.value = items.value.filter(i => i.id !== id)
}
// Proporciona los datos de solo lectura + funciones para mutarlos
// Esto sigue el principio de que el padre es dueño de su estado
provide(CARRITO_KEY, {
items: readonly(items),
agregarItem,
eliminarItem,
})
</script>readonly — proteger los datos proporcionados
Usar readonly() alrededor de un ref antes de proporcionarlo evita que los componentes hijos muten el estado directamente:
import { ref, readonly, provide } from 'vue'
const estado = ref({ contador: 0 })
// Los consumers pueden leer pero no mutar directamente
provide('estado', readonly(estado))
// Solo el provider puede mutar mediante las funciones que proporciona
provide('incrementar', () => { estado.value.contador++ })Patrón Provider Component
Un patrón común es crear un componente dedicado solo para proporcionar contexto:
<!-- ContextoFormulario.vue -->
<script setup lang="ts">
import { reactive, provide, computed } from 'vue'
import type { InjectionKey } from 'vue'
interface ContextoForm {
valores: Record<string, string>
errores: Record<string, string>
registrar: (campo: string) => void
validar: (campo: string, valor: string) => void
obtenerError: (campo: string) => string
}
export const FORM_KEY: InjectionKey<ContextoForm> = Symbol('form')
const valores = reactive<Record<string, string>>({})
const errores = reactive<Record<string, string>>({})
function registrar(campo: string): void {
if (!(campo in valores)) valores[campo] = ''
}
function validar(campo: string, valor: string): void {
valores[campo] = valor
errores[campo] = valor.trim() ? '' : `${campo} es requerido`
}
function obtenerError(campo: string): string {
return errores[campo] ?? ''
}
provide(FORM_KEY, { valores, errores, registrar, validar, obtenerError })
</script>
<template>
<form @submit.prevent>
<slot />
</form>
</template>Cuándo usar provide/inject vs otras opciones
| Herramienta | Cuándo usarla |
|---|---|
| Props | Comunicación directa padre-hijo con datos simples |
| Emits | Comunicación hijo-padre para eventos |
| provide/inject | Jerarquía clara, datos específicos de un árbol de componentes |
| Pinia | Estado verdaderamente global, acceso desde cualquier lugar, devtools |
| Composables | Lógica reutilizable sin necesidad de compartir estado entre instancias |
Evitar el abuso de provide/inject
Aunque provide/inject es poderoso, puede hacer el código difícil de seguir si se abusa. Algunos principios:
- Documenta qué se proporciona: El
InjectionKeycon el tipo correcto sirve como documentación - Proporciona interfaces, no implementaciones: Proporciona funciones y datos, no el componente completo
- Valida en inject(): Siempre verifica que el valor no sea
undefinedsi el provider puede no estar presente - Prefiere Pinia para estado global: Si muchos componentes no relacionados necesitan el dato, usa Pinia
Práctica
- Sistema de tema: Crea un
TemaProvider.vueque proporcione el tema actual (claro/oscuro) usandoInjectionKey. Crea unBotonTema.vueque useinject()para acceder al tema y cambiarlo. - Contexto de formulario: Implementa el patrón Provider Component para un formulario. Cada campo hijo usa
inject()para registrarse y reportar errores al formulario padre. - Toast provider: Crea un sistema de notificaciones toast donde
AppProvider.vueproporciona una funciónmostrarToast(mensaje, tipo). Cualquier componente puede inyectar y llamar a esta función para mostrar notificaciones.
En la siguiente lección aprenderemos Vue Router para construir aplicaciones multipágina.
import { type InjectionKey, type Ref } from 'vue'
// InjectionKey garantiza el tipado completo entre provide e inject
export const TEMA_KEY: InjectionKey<{
tema: Ref<'claro' | 'oscuro'>
toggleTema: () => void
}> = Symbol('tema')
export const USUARIO_KEY: InjectionKey<{
nombre: Ref<string>
rol: Ref<'admin' | 'usuario'>
cerrarSesion: () => void
}> = Symbol('usuario')
export const LOCALE_KEY: InjectionKey<Ref<string>> = Symbol('locale')
Inicia sesión para guardar tu progreso