Passare i Dati in Profondità con il Context
Solitamente, passerai le informazioni da un componente genitore a un componente figlio tramite le props. Tuttavia, passare le props può diventare verboso e scomodo se fatto attraverso molti componenti intermedi, o se molti componenti nella tua app necessitano della stessa informazione. Il context permette al componente genitore di rendere disponibile un’informazione a qualsiasi componente nell’albero sottostante, a prescindere dalla sua profondità, senza passarla esplicitamente tramite props.
Imparerai
- Cos’è il “prop drilling”
- Come sostituire il passaggio di props ripetitive con il context
- Casi d’uso comuni del context
- Alternative comuni al context
Il problema di passare le props
Passare le props è un ottimo modo per convogliare esplicitamente i dati attraverso l’albero della UI verso i componenti che ne fanno uso.
Tuttavia, passare le props può diventare verboso e scomodo quando devi farlo in profondità nell’albero, o se molti componenti necessitano della stessa prop. Il parente comune più vicino potrebbe essere molto distante dai componenti che necessitano dei dati, e sollevare lo state in alto fino a quel punto può portare a una situazione chiamata “prop drilling”.
Non sarebbe fantastico se ci fosse un modo per “teletrasportare” i dati ai componenti che ne hanno bisogno senza passare le props? Con la funzionalità context di React, questo è possibile!
Context: un’alternativa al passaggio delle props
Il context consente a un componente genitore di fornire dati a tutto l’albero sottostante. Ci sono molteplici utilizzi per il context. Ecco un esempio: considera questo componente Heading
che accetta un level
per la sua dimensione:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Title</Heading> <Heading level={2}>Heading</Heading> <Heading level={3}>Sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={5}>Sub-sub-sub-heading</Heading> <Heading level={6}>Sub-sub-sub-sub-heading</Heading> </Section> ); }
Ipotizziamo che tu voglia la stessa dimensione per i titoli di una medesima Section
.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Title</Heading> <Section> <Heading level={2}>Heading</Heading> <Heading level={2}>Heading</Heading> <Heading level={2}>Heading</Heading> <Section> <Heading level={3}>Sub-heading</Heading> <Heading level={3}>Sub-heading</Heading> <Heading level={3}>Sub-heading</Heading> <Section> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Attualmente, passi la prop level
separatamente a ogni <Heading>
.
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
Sarebbe bello se potessi passare la prop level
al componente <Section>
e rimuoverla da <Heading>
. In questo modo, potresti garantire che tutti i titoli nella stessa sezione abbiano la stessa dimensione:
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
Ma come può il componente <Heading>
conoscere il livello della sua <Section>
più vicina? Ciò richiederebbe un modo per consentire a un figlio di “chiedere” dati da un posto più in alto nell’albero.
Non puoi farlo solamente con le props, ed è qui che entra in gioco il context. Procederai in tre passaggi:
- Crea un context. (Puoi chiamarlo
LevelContext
, dal momento che è per il livello d’intestazione.) - Usa quel context dal componente che ha bisogno del dato. (
Heading
useràLevelContext
.) - Fornisci quel context dal componente che specifica il dato. (
Section
forniràLevelContext
.)
Context permette a un genitore, anche se distante, di fornire dei dati a tutto l’albero al suo interno.
Step 1: Creare il context
Prima di tutto, devi creare il context. Dovrai esportarlo da un file in modo che i tuoi componenti possano utilizzarlo:
import { createContext } from 'react'; export const LevelContext = createContext(1);
L’unico argomento di createContext
è il valore predefinito. Qui, 1
si riferisce al livello d’intestazione più alto, ma potresti passare qualsiasi tipo di valore (anche un oggetto). Vedrai il significato del valore predefinito nel prossimo passo.
Step 2: Usa il context
Importa l’Hook useContext
da React e il tuo context:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
Attualmente, il componente Heading
legge level
dalle props:
export default function Heading({ level, children }) {
// ...
}
Piuttosto, rimuovi la prop level
e leggi il valore dal context che hai appena importato, LevelContext
:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
è un Hook. Proprio come useState
e useReducer
, puoi chiamare un Hook solo immediatamente all’interno di un componente React (non all’interno di cicli o condizioni). useContext
indica a React che il componente Heading
desidera leggere il LevelContext
.
Ora che il componente Heading
non ha una prop level
, non hai più bisogno di passarla a Heading
nel tuo JSX:
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
Modifica il JSX in modo che sia invece Section
a riceverlo:
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
Come promemoria, questo è il markup che stavi cercando di far funzionare:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Title</Heading> <Section level={2}> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section level={3}> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section level={4}> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Come puoi notare questo esempio non funziona ancora! Tutti i titoli hanno la stessa dimensione perché, anche se stai usando il context, non lo hai ancora fornito. React non sa da dove prenderlo!
Se non fornisci il context, React utilizzerà il valore predefinito specificato nel passaggio precedente. In questo esempio, hai specificato 1
come argomento di createContext
, quindi useContext(LevelContext)
restituisce 1
, impostando tutti i titoli su <h1>
. Risolviamo questo problema facendo sì che ciascuna Section
fornisca il proprio context.
Step 3: Fornisci il context
Il componente Section
attualmente renderizza i propri figli:
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
Avvolgili con un context provider per fornire loro il LevelContext
:
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
Questo dice a React: “se un qualsiasi componente all’interno di questa <Section>
richiede LevelContext
, fornisci loro questo level
.” Il componente utilizzerà il valore del <LevelContext.Provider>
più vicino nell’albero della UI sopra di esso.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Title</Heading> <Section level={2}> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section level={3}> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section level={4}> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
È lo stesso risultato del codice originale, ma non hai dovuto passare la prop level
a ciascun componente Heading
! Invece, questo “capisce” il suo livello d’intestazione interrogando il Section
più vicino sopra di esso:
- Passi la prop
level
a<Section>
. Section
avvolge i suoi figli in<LevelContext.Provider value={level}>
.Heading
richiede il valore più vicino diLevelContext
sopra di sé conuseContext(LevelContext)
.
Usare e fornire un context dallo stesso componente
Attualmente, devi ancora specificare il level
di ogni sezione manualmente:
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
Poiché il context ti consente di leggere informazioni da un componente superiore, ogni Section
potrebbe leggere il level
dal Section
superiore e passare level + 1
automaticamente verso il basso. Ecco come potresti farlo:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
Con questa modifica, non devi più passare la prop level
né a <Section>
né a <Heading>
:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Title</Heading> <Section> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Ora sia Heading
che Section
leggono il LevelContext
per capire quanto sono “profondi”. E la Section
avvolge i suoi figli in LevelContext
per specificare che tutto ciò che è all’interno di essa si trova a un livello “più profondo”.
Passaggi di Context attraverso componenti intermediari
Puoi inserire quanti componenti desideri tra il componente che fornisce il context e quello che lo utilizza. Questo include sia componenti integrati come <div>
che componenti costruiti da te.
In questo esempio, lo stesso componente Post
(con un bordo tratteggiato) viene renderizzato a due diversi livelli di nidificazione. Nota che il <Heading>
al suo interno ottiene automaticamente il suo livello dalla <Section>
più vicina:
import Heading from './Heading.js'; import Section from './Section.js'; export default function ProfilePage() { return ( <Section> <Heading>My Profile</Heading> <Post title="Hello traveller!" body="Read about my adventures." /> <AllPosts /> </Section> ); } function AllPosts() { return ( <Section> <Heading>Posts</Heading> <RecentPosts /> </Section> ); } function RecentPosts() { return ( <Section> <Heading>Recent Posts</Heading> <Post title="Flavors of Lisbon" body="...those pastéis de nata!" /> <Post title="Buenos Aires in the rhythm of tango" body="I loved it!" /> </Section> ); } function Post({ title, body }) { return ( <Section isFancy={true}> <Heading> {title} </Heading> <p><i>{body}</i></p> </Section> ); }
Non hai dovuto fare nulla di speciale affinché ciò funzionasse. Una Section
specifica il context per l’albero al suo interno, quindi puoi inserire un <Heading>
ovunque e avrà la dimensione corretta. Fai una prova nella sandbox qui sopra!
**Il context ti permette di scrivere componenti che “si adattano all’ambiente circostante” e si mostrano in maniera diversa a seconda di dove (o, in altre parole, in quale context) vengono renderizzati.
Il funzionamento del context potrebbe ricordarti l’ereditarietà delle proprietà CSS. In CSS, puoi specificare color: blue
per un <div>
, e qualsiasi nodo DOM all’interno di esso, a qualsiasi profondità, erediterà quel colore a meno che qualche altro nodo DOM nel mezzo non lo sovrascriva con color: green
. In maniera simile, in React, l’unico modo per sovrascrivere un context proveniente dall’alto è avvolgere i figli in un context provider con un valore diverso.
In CSS, diverse proprietà come color
e background-color
non si sovrascrivono a vicenda. Puoi impostare il color
di tutti i <div>
su rosso senza impattare sul background-color
. In maniera simile, diversi context React non si sovrascrivono a vicenda. Ogni context che crei con createContext()
è completamente separato dagli altri e lega i componenti che utilizzano e forniscono quello specifico context. Un componente può utilizzare o fornire molti context diversi senza problemi.
Prima che usi il context
Il context è molto allettante da usare! Tuttavia, ciò significa anche che è troppo facile abusarne. Il fatto che tu debba passare alcune props a vari livelli di profondità non significa necessariamente che dovresti mettere quelle informazioni in un context.
Ecco alcune alternative che dovresti considerare prima di utilizzare il context:
- Inizia passando le props. Se i tuoi componenti non sono banali, non è insolito passare una dozzina di props attraverso una dozzina di componenti. Potrebbe sembrare un lavoro noioso, ma rende molto chiaro quali componenti utilizzano quali dati! La persona che manterrà il tuo codice sarà grata che tu abbia reso esplicito il flusso dei dati tramite le props.
- Estrai i componenti e passa JSX come
children
a essi. Se passi alcuni dati attraverso molti strati di componenti intermedi che non utilizzano quei dati (e li passano solo più in basso), spesso significa che hai dimenticato di estrarre alcuni componenti lungo la strada. Ad esempio, forse passi props di dati comeposts
a componenti visivi che non li utilizzano direttamente, come<Layout posts={posts} />
. Invece, fai in modo cheLayout
accettichildren
come prop e renderizza<Layout><Posts posts={posts} /></Layout>
. Questo riduce il numero di strati tra il componente che specifica i dati e quello che ne ha bisogno.
Se nessuno di questi approcci fa al caso tuo, considera il context.
Casi d’uso del context
- Temi: Se la tua app consente all’utente di cambiare l’aspetto (ad esempio la modalità scura), puoi inserire un context provider alla radice della tua app e utilizzare quel context nei componenti che devono adattare il loro aspetto visivo.
- Account corrente: Molti componenti potrebbero avere bisogno di conoscere l’utente attualmente loggato. Metterlo nel context lo rende comodo da leggere ovunque nell’albero. Alcune app consentono anche di operare con più account contemporaneamente (ad esempio, per lasciare un commento come un utente diverso). In questi casi, può essere comodo avvolgere una parte dell’interfaccia utente in un provider nidificato con un valore di account diverso.
- Routing: La maggior parte delle soluzioni di routing utilizza internamente il context per memorizzare la rotta corrente. È così che ogni collegamento “sa” se è attivo o no. Se crei il tuo router, potresti voler fare lo stesso.
- Gestione dello state: Man mano che la tua app cresce, potresti finire con molto state vicino alla radice dell’app. Molti componenti distanti al di sotto potrebbero volerlo modificare. È comune usare un reducer insieme al context per gestire uno state complesso e passarlo in basso a componenti distanti senza troppi sforzi.
Il context non è limitato a valori statici. Se passi un valore diverso nella renderizzazione successiva, React aggiornerà tutti i componenti sottostanti che lo leggono! Ecco perché il context spesso è utilizzato in combinazione con lo state.
In generale, se alcune informazioni sono necessarie da componenti distanti in diverse parti dell’albero, quello è un buon indicatore che il context ti aiuterà.
Riepilogo
- Il context consente a un componente di fornire alcune informazioni a tutto l’albero sottostante:
- Per passare il context:
- Crealo ed esportalo con
export const MyContext = createContext(defaultValue)
. - Passalo all’Hook
useContext(MyContext)
per leggerlo in qualsiasi componente figlio, indipendentemente da quando in profondità sia. - Avvolgi i figli in
<MyContext.Provider value={...}>
per fornirlo da un genitore.
- Crealo ed esportalo con
- Il context attraversa qualsiasi componente intermedio.
- Il context ti consente di scrivere componenti che “si adattano all’ambiente circostante”.
- Prima di utilizzare il context, prova a passare le props o passare il JSX come
children
.
Sfida 1 di 1: Sostituisci il prop drilling con il context
In questo esempio, selezionare la checkbox modifica la prop imageSize
passata a ciascun <PlaceImage>
. Lo state della checkbox è contenuto nel componente di livello superiore App
, ma ogni <PlaceImage>
deve essere consapevole di esso.
Attualmente, App
passa imageSize
a List
, che lo passa a ciascun Place
, che lo passa a PlaceImage
. Rimuovi la prop imageSize
e, invece, passala direttamente dal componente App
a PlaceImage
.
Puoi dichiarare il context in Context.js
.
import { useState } from 'react'; import { places } from './data.js'; import { getImageUrl } from './utils.js'; export default function App() { const [isLarge, setIsLarge] = useState(false); const imageSize = isLarge ? 150 : 100; return ( <> <label> <input type="checkbox" checked={isLarge} onChange={e => { setIsLarge(e.target.checked); }} /> Use large images </label> <hr /> <List imageSize={imageSize} /> </> ) } function List({ imageSize }) { const listItems = places.map(place => <li key={place.id}> <Place place={place} imageSize={imageSize} /> </li> ); return <ul>{listItems}</ul>; } function Place({ place, imageSize }) { return ( <> <PlaceImage place={place} imageSize={imageSize} /> <p> <b>{place.name}</b> {': ' + place.description} </p> </> ); } function PlaceImage({ place, imageSize }) { return ( <img src={getImageUrl(place)} alt={place.name} width={imageSize} height={imageSize} /> ); }