En esta página

provide() e inject() — inyección de dependencias

12 min lectura TextoCap. 3 — Composición

¿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 usuario

Sin 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:

  1. Documenta qué se proporciona: El InjectionKey con el tipo correcto sirve como documentación
  2. Proporciona interfaces, no implementaciones: Proporciona funciones y datos, no el componente completo
  3. Valida en inject(): Siempre verifica que el valor no sea undefined si el provider puede no estar presente
  4. Prefiere Pinia para estado global: Si muchos componentes no relacionados necesitan el dato, usa Pinia

Práctica

  1. Sistema de tema: Crea un TemaProvider.vue que proporcione el tema actual (claro/oscuro) usando InjectionKey. Crea un BotonTema.vue que use inject() para acceder al tema y cambiarlo.
  2. 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.
  3. Toast provider: Crea un sistema de notificaciones toast donde AppProvider.vue proporciona una función mostrarToast(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.

Siempre usa InjectionKey para tipado seguro
Usar Symbol como clave sin InjectionKey hace que inject() devuelva unknown. Al tipar la clave con InjectionKey<T>, TypeScript sabe exactamente qué tipo devuelve inject() y autocompletará correctamente tanto en el provide como en el inject.
provide/inject no es reactivo por sí solo — proporciona refs
Si proporcionas un valor primitivo (un string, un número), no será reactivo. Para que los cambios se propaguen a los componentes que inyectan, siempre proporciona refs o reactive: provide(CLAVE, ref('valor')). Los componentes que inyecten recibirán el ref y verán los cambios.
provide/inject vs Pinia — cuándo usar cada uno
Usa provide/inject cuando tienes una jerarquía clara de componentes y el dato es específico de esa sub-árbol (un FormularioProvider que provee contexto a sus campos, un ModalProvider). Usa Pinia cuando el estado es genuinamente global, necesitas devtools, persistencia o acceso desde cualquier parte de la app.
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')