Provide e Inject
Il problema del prop drilling
Quando devi passare dati da un componente genitore a un componente molto annidato, dovresti passare le props attraverso ogni livello intermedio. Questo pattern, chiamato prop drilling, rende il codice verboso e difficile da mantenere.
ComponenteA (ha i dati)
└── ComponenteB (passa i dati, non li usa)
└── ComponenteC (passa i dati, non li usa)
└── ComponenteD (usa i dati)
provide e inject risolvono questo problema permettendo di passare dati direttamente da un antenato a qualsiasi discendente.
provide()
provide() rende un valore disponibile a tutti i componenti discendenti.
<!-- ComponenteAntenato.vue -->
<script setup lang="ts">
import { provide, ref, readonly } from 'vue'
const tema = ref<'chiaro' | 'scuro'>('chiaro')
const nomeApp = 'La Mia App'
// Provide di un valore semplice
provide('nomeApp', nomeApp)
// Provide di un valore reattivo
provide('tema', tema)
// Provide di funzioni
function cambiaTema() {
tema.value = tema.value === 'chiaro' ? 'scuro' : 'chiaro'
}
provide('cambiaTema', cambiaTema)
// Provide di un valore readonly (consigliato per sicurezza)
provide('temaReadonly', readonly(tema))
</script>
<template>
<div :class="tema">
<h1>{{ nomeApp }}</h1>
<slot />
</div>
</template>
inject()
inject() recupera un valore fornito da un antenato.
<!-- ComponenteDiscendente.vue (a qualsiasi profondita) -->
<script setup lang="ts">
import { inject, type Ref } from 'vue'
// Inject base
const nomeApp = inject<string>('nomeApp')
// Inject con valore predefinito
const tema = inject<Ref<string>>('tema', ref('chiaro'))
// Inject di una funzione
const cambiaTema = inject<() => void>('cambiaTema', () => {})
</script>
<template>
<div>
<p>App: {{ nomeApp }}</p>
<p>Tema: {{ tema }}</p>
<button @click="cambiaTema">Cambia tema</button>
</div>
</template>
Reattivita con provide/inject
Per mantenere la reattivita, fornisci ref o reactive. Le modifiche nel provider si propagano automaticamente a tutti gli inject.
<!-- ProviderUtente.vue -->
<script setup lang="ts">
import { provide, ref, computed } from 'vue'
interface Utente {
id: number
nome: string
email: string
ruolo: 'admin' | 'utente'
}
const utenteCorrente = ref<Utente | null>(null)
const isAutenticato = computed(() => utenteCorrente.value !== null)
const isAdmin = computed(() => utenteCorrente.value?.ruolo === 'admin')
async function login(email: string, password: string) {
const risposta = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
})
utenteCorrente.value = await risposta.json()
}
function logout() {
utenteCorrente.value = null
}
// Provide di un oggetto strutturato con dati e azioni
provide('auth', {
utente: utenteCorrente,
isAutenticato,
isAdmin,
login,
logout
})
</script>
<template>
<slot />
</template>
<!-- Qualsiasi discendente puo usare i dati di autenticazione -->
<script setup lang="ts">
import { inject, type Ref, type ComputedRef } from 'vue'
interface AuthContext {
utente: Ref<any>
isAutenticato: ComputedRef<boolean>
isAdmin: ComputedRef<boolean>
login: (email: string, password: string) => Promise<void>
logout: () => void
}
const auth = inject<AuthContext>('auth')!
// Ora puoi usare auth.utente, auth.isAutenticato, ecc.
</script>
<template>
<div v-if="auth.isAutenticato.value">
<p>Benvenuto, {{ auth.utente.value.nome }}</p>
<button @click="auth.logout">Logout</button>
</div>
<div v-else>
<p>Effettua il login</p>
</div>
</template>
Symbol keys
Per evitare collisioni di nomi, usa Symbol come chiave di provide/inject. Questo e il pattern consigliato per librerie e progetti grandi.
// chiavi-injection.ts
import type { InjectionKey, Ref, ComputedRef } from 'vue'
// Definisci interfacce
export interface TemaContext {
tema: Ref<'chiaro' | 'scuro'>
cambia: () => void
colori: ComputedRef<{ sfondo: string; testo: string }>
}
export interface NotificaContext {
mostra: (messaggio: string, tipo?: 'successo' | 'errore' | 'info') => void
nascondi: (id: number) => void
}
// Crea chiavi tipizzate con Symbol
export const TEMA_KEY: InjectionKey<TemaContext> = Symbol('tema')
export const NOTIFICA_KEY: InjectionKey<NotificaContext> = Symbol('notifica')
<!-- Provider con Symbol key -->
<script setup lang="ts">
import { provide, ref, computed } from 'vue'
import { TEMA_KEY, type TemaContext } from '@/chiavi-injection'
const tema = ref<'chiaro' | 'scuro'>('chiaro')
const colori = computed(() => {
if (tema.value === 'scuro') {
return { sfondo: '#1a1a2e', testo: '#e0e0e0' }
}
return { sfondo: '#ffffff', testo: '#333333' }
})
function cambia() {
tema.value = tema.value === 'chiaro' ? 'scuro' : 'chiaro'
}
// TypeScript sa esattamente cosa deve avere l'oggetto
provide(TEMA_KEY, { tema, cambia, colori })
</script>
<!-- Consumer con Symbol key -->
<script setup lang="ts">
import { inject } from 'vue'
import { TEMA_KEY } from '@/chiavi-injection'
// TypeScript conosce automaticamente il tipo
const temaContext = inject(TEMA_KEY)
if (!temaContext) {
throw new Error('TEMA_KEY non fornito. Assicurati di usare TemaProvider.')
}
const { tema, cambia, colori } = temaContext
</script>
<template>
<div :style="{ background: colori.sfondo, color: colori.testo }">
<p>Tema: {{ tema }}</p>
<button @click="cambia">Cambia tema</button>
</div>
</template>
App-level provide
Puoi fornire valori a livello di applicazione in main.ts, rendendoli disponibili a tutti i componenti.
// main.ts
import { createApp, ref } from 'vue'
import App from './App.vue'
import { TEMA_KEY, NOTIFICA_KEY } from './chiavi-injection'
const app = createApp(App)
// Provide a livello app
app.provide(TEMA_KEY, {
tema: ref('chiaro'),
cambia: () => { /* ... */ },
colori: computed(() => ({ sfondo: '#fff', testo: '#333' }))
})
// Provide della configurazione globale
app.provide('config', {
apiUrl: import.meta.env.VITE_API_URL,
appNome: 'La Mia App',
versione: '1.0.0'
})
app.mount('#app')
Pattern composable con provide/inject
Il pattern piu pulito e creare composables che incapsulano provide e inject.
// composables/useTema.ts
import { provide, inject, ref, computed, type Ref, type ComputedRef } from 'vue'
interface TemaState {
tema: Ref<'chiaro' | 'scuro'>
isDark: ComputedRef<boolean>
cambia: () => void
imposta: (t: 'chiaro' | 'scuro') => void
}
const TEMA_SYMBOL = Symbol('tema')
// Chiamato dal componente provider (una sola volta)
export function provideTema(temaIniziale: 'chiaro' | 'scuro' = 'chiaro') {
const tema = ref(temaIniziale)
const isDark = computed(() => tema.value === 'scuro')
function cambia() {
tema.value = tema.value === 'chiaro' ? 'scuro' : 'chiaro'
}
function imposta(t: 'chiaro' | 'scuro') {
tema.value = t
}
const state: TemaState = { tema, isDark, cambia, imposta }
provide(TEMA_SYMBOL, state)
return state
}
// Chiamato da qualsiasi discendente
export function useTema(): TemaState {
const state = inject<TemaState>(TEMA_SYMBOL)
if (!state) {
throw new Error(
'useTema() richiede provideTema() in un componente antenato'
)
}
return state
}
<!-- App.vue - Provider -->
<script setup>
import { provideTema } from '@/composables/useTema'
const { tema, isDark } = provideTema('chiaro')
</script>
<template>
<div :class="{ dark: isDark }">
<router-view />
</div>
</template>
<!-- Qualsiasi componente discendente -->
<script setup>
import { useTema } from '@/composables/useTema'
const { tema, cambia, isDark } = useTema()
</script>
<template>
<button @click="cambia">
{{ isDark ? 'Tema chiaro' : 'Tema scuro' }}
</button>
</template>
Provide/inject vs Pinia
| Caratteristica | provide/inject | Pinia |
|---|---|---|
| Ambito | Albero componenti | Globale app |
| DevTools | Limitato | Completo |
| SSR | Da gestire | Supporto nativo |
| Complessita | Bassa | Media |
| Uso ideale | Contesto locale, configurazione | Stato globale, dati condivisi |
| Persistenza | No | Con plugin |
Best practice
- Usa Symbol keys: Prevengono collisioni di nomi e abilitano la tipizzazione
- Fornisci dati readonly quando possibile: Previeni modifiche accidentali con
readonly() - Crea composables provide/inject: Incapsula provider e consumer in funzioni riutilizzabili
- Lancia errori se inject fallisce: Fornisci messaggi chiari quando il provider manca
- Non abusare di provide/inject: Usa Pinia per stato veramente globale
- Documenta le dipendenze: Chi usa inject deve sapere quale provider e necessario
- Mantieni il provider vicino ai consumer: Non mettere tutto a livello app se non necessario