Passare le props attraverso strati di componenti non interessati è un lavoro noioso. Un giorno stai rilasciando una funzionalità e il giorno dopo ti ritrovi a modificare sei file solo per rinominare una singola prop. Questo è il prop drilling in sintesi: un genitore ha dei dati, un figlio profondo ne ha bisogno, e ogni componente tra loro diventa un corriere. L'app funziona ancora, ma il codebase diventa fragile. Rimuovi uno strato intermedio e metà dell'albero crolla. Cambia un tipo e TypeScript si lamenterà in tre directory diverse. La React Context API esiste proprio per eliminare completamente questi intermediari.
Cosa significa realmente il Prop Drilling
Immagina una struttura standard di un'app. Hai un componente App che recupera l'utente corrente. Dentro App vive un Layout, dentro Layout si trova una Sidebar, dentro Sidebar è annidata una Navigation e, infine, dentro Navigation trovi il UserAvatar che ha effettivamente bisogno dell'oggetto utente.
Il tuo codice finirà per essere così:
function App() {
const user = { name: 'Aarav', role: 'admin' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <Navigation user={user} />;
}
function Navigation({ user }) {
return <UserAvatar user={user} />;
}
Layout, Sidebar e Navigation non fanno nulla con quell'oggetto utente se non passarlo più in basso. Accumulano props che non possiedono, le loro interfacce si gonfiano e testarli richiede il mocking di dati che non toccano mai. Il vero problema è la velocità con cui questo fenomeno si diffonde. Aggiungi un flag isLoggedIn, una stringa locale o un valore theme, e la stessa sfilata si ripete.
Come la Context API cambia le regole del gioco
Pensa alla Context API come a un router WiFi. Invece di far passare lunghi cavi in ogni stanza per raggiungere ogni dispositivo, il router invia un segnale nell'aria. Qualsiasi dispositivo nel raggio d'azione può connettersi direttamente. In termini React, il router è il Provider, il segnale è il tuo stato o i tuoi dati, e il dispositivo è qualsiasi componente annidato che chiama useContext.
La configurazione ha tre componenti fondamentali:
React.createContext()crea il canale dati.- Il Provider avvolge una sezione del tuo albero e trasmette un valore.
- L'hook
useContextpermette ai discendenti di ricevere quel valore senza toccare le props intermedie.
Hai ancora un albero, ma i rami tra la radice e la foglia non devono più concordare un contratto di corriere.
Costruire un Context da zero
Costruiamo un esempio concreto con le impostazioni del tema, dato che la maggior parte delle app ha bisogno di una modalità chiara o scura in qualche momento.
Per prima cosa, crea l'oggetto context. Questo è il tubo:
import { createContext, useState, useMemo } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeContext;
Poi avvolgi la tua applicazione nel provider. Di solito questo accade vicino alla radice:
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
Ora qualsiasi discendente può intercettare il segnale direttamente. Ecco un pulsante di attivazione (toggle) sepolto in profondità nell'interfaccia utente:
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}
>
Current theme: {theme}
</button>
);
}
Nota che Layout, Sidebar e Navigation non vedono mai la prop theme. Vengono renderizzati normalmente e ThemeToggle prende ciò di cui ha bisogno direttamente dal context. Il cablaggio è invisibile dall'esterno, ed è esattamente questo il punto.
Dove si inserisce correttamente il Context
Il Context funziona meglio per i dati che molti componenti distanti condividono, ma che nessun singolo genitore possiede in modo pulito. I candidati ideali includono:
- Impostazioni del tema come la modalità chiara o scura, i colori di accento o la scala dei font.
- Stato di autenticazione come l'oggetto utente corrente, lo stato di login o la scadenza della sessione.
- Lingua e localizzazione per l'internazionalizzazione.
- Dati del carrello della spesa che devono rimanere sincronizzati tra un badge nell'header, un menu a discesa del mini-carrello e una pagina di checkout.
Resisti alla tentazione di scaricare ogni pezzo di stato locale nel Context. Un input di un modulo due livelli più in basso non ha bisogno di una trasmissione globale. Riserva il Context per vere problematiche trasversali (cross-cutting concerns) e lascia il resto come normali props.
Trappole di performance e come evitarle
Il Context non è gratuito. Quando un valore del context si aggiorna, ogni componente collegato a quel context viene renderizzato nuovamente, anche se la porzione di valore che gli interessa non è cambiata. L'errore classico è inserire un nuovo letterale di oggetto nel Provider a ogni render del genitore.
Nel nostro esempio del tema, ogni volta che il ThemeProvider viene renderizzato nuovamente perché il suo genitore si è aggiornato, l'espressione { theme, setTheme } crea un oggetto completamente nuovo. React vede un nuovo riferimento e ogni consumatore si aggiorna. Se il tuo tema cambia raramente ma lo stato della tua app cambia spesso, stai pagando per render di cui non hai bisogno.
La soluzione è duplice.
Dividi i tuoi context in base alla frequenza di aggiornamento. Un UserContext che cambia una volta per login non dovrebbe condividere un provider con un NotificationContext che si aggiorna ogni pochi secondi. Mantienili separati in modo che i dati statici non subiscano lo stesso treno di re-render dei dati volatili.
Avvolgi il valore in useMemo quando il valore è un oggetto o un array. Fornisci a React un riferimento stabile:
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Ora l'identità dell'oggetto cambia solo quando theme cambia effettivamente. I componenti discendenti che necessitano del contesto ma sono protetti da React.memo più in basso salteranno il lavoro.
Errori che ti fanno perdere tempo
I due errori che compaiono ancora nel codice di produzione sono facili da prevenire.
Primo, dimenticare di esportare il contesto stesso. Se esporti solo il wrapper Provider e mantieni privato l'oggetto context, uno sviluppatore che scrive una nuova funzionalità non potrà chiamare useContext senza rifattorizzare il tuo modulo. Esporta il contesto in modo che i consumatori possano importare sia il provider che l'hook di consumo in modo pulito.
Secondo, chiamare useContext al di fuori del Provider corrispondente. Se ThemeToggle viene renderizzato in un ramo dell'albero che non è avvolto in ThemeProvider, l'hook restituirà il valore predefinito passato a createContext, o undefined se non ne hai passato nessuno. Ciò porta a fallimenti silenziosi come cannot read property of undefined. Puoi proteggerti da questo assegnando un valore predefinito sensato o lanciando un errore chiaro all'inizio della chiamata dell'hook.
Context vs Redux: Mantieni le cose semplici
Non hai sempre bisogno di Redux. Per progetti di medie dimensioni, Context abbinato a useState o useReducer copre la maggior parte della condivisione dello stato. Redux brilla quando hai bisogno del time-travel debugging, di middleware complessi o di transazioni globali che devono subire un rollback in sequenza. Se la tua intera gestione dello stato consiste in un oggetto utente, una stringa per il tema e un array del carrello, una libreria di store aggiunge solo boilerplate che non sfrutterai mai.
Detto questo, Context non è di per sé un sistema completo di gestione dello stato. Non fornisce un singolo snapshot globale e non raggruppa gli aggiornamenti tra contesti non correlati. Usalo come sostituto del prop drilling, non come un sistema operativo per l'intero livello dei dati.
La lezione principale
Smetti di passare le prop attraverso componenti che non ne hanno bisogno. Crea contesti mirati per i dati che effettivamente attraversano l'albero, avvolgi i provider abbastanza in alto da coprire i consumatori e stabilizza sempre l'oggetto valore quando passi collezioni o funzioni. La Context API mantiene il tuo codice React diretto: le prop rimangono locali, i dati globali viaggiano senza fili e i confini dei tuoi componenti rimangono puliti.
