Tiedon välittäminen syvälle kontekstilla
Useiten tiedon välittäminen tapahtuu pääkomponentilta alakomponentille propsien avulla. Propsien välittäminen voi kuitenkin tulla monimutkaiseksi ja hankalaksi, jos sinun täytyy välittää propseja useiden komponenttien läpi tai jos sovelluksessasi on useita komponentteja, jotka tarvitsevat samaa tietoa. Konteksti (engl. Context) antaa pääkomponentin tarjota mille tahansa sen alla olevan puun komponentille, vaikka ne olisivatkin syvällä puussa, ilman että tieto välitetään propsien kautta.
Tulet oppimaan
- Mitä “propsien poraus” on
- Miten korvata toistuva propsien välittäminen kontekstilla
- Yleisiä käyttötapauksia kontekstille
- Yleisiä vaihtoehtoja kontekstille
Ongelma propseja välittäessä
Propsien välittäminen on hyvä tapa välittää tietoa UI puun läpi komponenteille, jotka sitä tarvitsevat.
Propsien välittäminen voi kuitenkin muodostua pitkäksi ja epäkäteväksi, jos sinun täytyy välittää propseja syvälle puussa tai jos useat komponentit tarvitsevat samaa propsia. Lähin yhteinen komponentti saattaa olla kaukana komponentista, joka tarvitsee tietoa, ja tilan nostaminen ylös voi johtaa tilanteeseen, jota kutsutaan joskus “propsien poraukseksi” (engl. prop drilling).
Eikö olisikin hienoa, jos olisi tapa “teleportata” tietoa puun komponentteihin, jotka sitä tarvitsevat, ilman että tieto välitetään propsien kautta? Reactin kontekstitoiminto on sellainen tapa!
Context: vaihtoehto propsien välittämiseen
Konteksti antaa pääkomponentin tarjota mille tahansa sen alla olevan puun komponentille. Kontekstille on monia käyttökohteita. Tässä on yksi esimerkki. Harkitse tätä Heading
komponenttia, joka hyväksyy level
propsin sen kooksi:
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> ); }
Sanotaan, että haluat useiden otsikoiden olevan saman kokoisia samassa Section
komponentissa:
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> ); }
Tällä hetkellä välität level
propsin jokaiselle <Heading>
komponentille erikseen:
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
Olisi hienoa, jos voisit välittää level
propsin <Section>
komponentille ja poistaa sen <Heading>
komponentista. Tällöin voisit pakottaa kaikki otsikot samassa <Section>
komponentissa olemaan saman kokoisia:
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
Mutta miten <Heading>
komponentti voi tietää sen lähimmän <Section>
komponentin tason? Tämä vaatisi jonkin tapaa, jolla lapsi voisi “kysyä” jotain dataa puun yläpuolella.
Et voi tehdä sitä vain propsien avulla. Tässä on, missä konteksti tulee mukaan. Tehdään se kolmessa vaiheessa:
- Luo konteksti. (Voit kutsua sitä
LevelContext
:ksi, koska se on otsiko tasoa varten.) - Käytä kontekstia komponentista, joka tarvitsee datan. (
Heading
käyttääLevelContext
:a.) - Tarjoa konteksti komponentista, joka määrittää datan. (
Section
tarjoaaLevelContext
:n.)
Konteksti antaa pääkomponentin - jopa kaukaisen - tarjota joitain tietoja koko puun sisällä.
1. Vaihe: Luo context
Ensiksi, sinun täytyy luoda konteksti. Sinun täytyy exporta se tiedostosta jotta komponentit voivat käyttää sitä:
import { createContext } from 'react'; export const LevelContext = createContext(1);
Ainoa argumentti createContext
:lle on sen oletusarvo. Tässä, 1
viittaa suurimpaan otsikon tasoon, mutta voit antaa minkä tahansa tyyppisen arvon (jopa olion). Näet oletusarvon merkityksen seuraavassa vaiheessa.
2. Vaihe: Käytä contextia
Importtaa kontekstisi ja useContext
Hook Reactista:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
Tällä hetkellä, Heading
komponentti lukee level
:n propseista:
export default function Heading({ level, children }) {
// ...
}
Sen sijaan, poista level
propsi ja lue arvo LevelContext
kontekstista, jonka juuri importoitit:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
on hookki. Juuri kuten useState
sekä useReducer
, voit kutsua hookkia vain React-komponentin ylimmällä tasolla (et silmukoiden tai ehtolauseiden sisällä). useContext
kertoo Reactille, että Heading
-komponentti haluaa lukea LevelContext
-kontekstin.
Nyt kun Heading
-komponentti ei enää sisällä level
-propsia, level
-propsia ei tarvitse enää välittää Heading
-komponentille JSX:ssä:
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
Päivitä JSX, jotta se on Section
, joka sen saa:
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
Muistutuksena, tämä on se merkintäkoodi, jota yritit saada toimimaan:
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> ); }
Huomaa, että tämä esimerkki ei vielä toimi! Kaikki otsikot ovat saman kokoisia, koska vaikka käytät kontekstia, et ole vielä tarjonnut sitä. React ei tiedä, mistä sen saa!
Jos et tarjoa kontekstia, React käyttää oletusarvoa, jonka määritit edellisessä vaiheessa. Tässä esimerkissä määritit 1
:n argumenttina createContext
:lle, joten useContext(LevelContext)
palauttaa 1
, asettaen kaikki otsikot <h1>
:ksi. Korjataan tämä ongelma antamalla jokaisen Section
-komponentin tarjota oma kontekstinsa.
3. Vaihe: Tarjoa context
Section
-komponentti renderöi lapsensa:
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
Kääri se kontekstitarjoajaan, jotta voit tarjota LevelContext
-kontekstin niille:
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
Tämä kertoo Reactille: “jos jokin komponentti tämän <Section>
-komponentin sisällä kysyy LevelContext
:iä, anna heille tämä level
.” Komponentti käyttää lähintä <LevelContext.Provider>
-komponenttia UI-puussa sen yläpuolella.
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> ); }
Tämä on sama tulos kuin alkuperäisessä koodissa, mutta sinun ei tarvinnut antaa level
-propsia jokaiselle Heading
-komponentille! Sen sijaan se “päättelee” otsikkotason kysymällä lähimmältä Section
-komponentilta yläpuolella:
- Välität
level
-propsin<Section>
-komponentille. Section
käärii lapsensa<LevelContext.Provider value={level}>
:n sisään.Heading
kysyy lähimmänLevelContext
:n arvonuseContext(LevelContext)
funktiolla.
Contextin käyttäminen ja tarjoaminen samasta komponentista
Tällä hetkellä sinun on vielä määritettävä jokaisen osion level
manuaalisesti:
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
Koska konteksti antaa sinun lukea tietoja komponentista yläpuolelta, jokainen Section
voisi lukea level
-arvon yläpuolelta olevasta Section
-komponentista ja välittää level + 1
alaspäin automaattisesti. Tässä on tapa tehdä se:
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>
);
}
Tällä muutoksella sinun ei tarvitse välittää level
-propsia kummallekaan <Section>
-komponentille tai <Heading>
-komponentille:
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> ); }
Nyt sekä Heading
että Section
lukevat LevelContext
:n selvittääkseen kuinka “syvällä” ne ovat. Ja Section
käärii lapsensa LevelContext
:n sisään määrittääkseen, että mikä tahansa sen sisällä on “syvemmällä” tasolla.
Konteksti välittyy välissä olevien komponenttien läpi
Voit lisätä niin monta komponenttia kuin haluat kontekstin tarjoavan komponentin ja sen käyttävän komponentin välille. Tämä sisältää sekä sisäänrakennetut komponentit kuten <div>
että itse luomiasi komponentteja.
Tässä esimerkissä, sama Post
-komponentti (jolla on katkoviiva) renderöidään kahdella eri tasolla. Huomaa, että <Heading>
-komponentti sen sisällä saa tasonsa automaattisesti lähimmästä <Section>
-komponentista:
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> ); }
Et tehnyt mitään erityistä, jotta tämä toimisi. Section
määrittelee kontekstin sen sisällä olevalle puulle, jotta voit sijoittaa <Heading>
komponentin mihin tahansa, ja se omaa silti oikean koon. Kokeile yllä olevassa hiekkalaatikossa!
Kontekstin avulla voit kirjoittaa komponentteja jotka “mukautuvat ympäristöönsä” ja näyttäytyvät eri tavalla riippuen siitä missä (tai, toisin kuvailtuna, missä kontekstissa) niitä renderöidään.
Miten konteksti toimii saattaa muistuttaa sinua CSS ominaisuuksien periytymisestä. CSS:ssä, voit määritellä color: blue
<div>
:lle, ja mikä tahansa DOM noodi sen sisällä, ei väliä miten syvällä, perii tämän värin ellei jokin toinen DOM noodi välissä ylikirjoita sitä color: green
:llä. Samoin, Reactissa, ainoa tapa ylikirjoittaa jokin konteksti joka tulee ylhäältä on kääriä lapset kontekstitarjoajan kanssa eri arvolla.
CSS:ssä eri ominaisuudet kuten color
ja background-color
eivät ylikirjoita toisiaan. Voit asettaa kaikkien <div>
:en color
:t punaiseksi ilman että se vaikuttaa background-color
:iin. Samoin, eri React kontekstit eivät ylikirjoita toisiaan. Jokainen konteksti, jonka luot createContext()
:lla on täysin erillinen muista, ja se yhdistää komponentteja jotka käyttävät ja tarjoavat tätä tiettyä kontekstia. Yksi komponentti voi käyttää tai tarjota monia eri konteksteja ongelmitta.
Ennen kuin käytät contextia
Kontekstia on erittäin houkuttelevaa käyttää! Tämä kuitenkin tarkoittaa myös sitä, että sitä on liian helppoa ylikäyttää. Vain siksi, että sinun täytyy välittää joitain propseja useita tasoja syvälle, ei tarkoita että sinun pitäisi laittaa tieto kontekstiin.
Tässä on muutama vaihtoehto, jotka sinun pitäisi harkita ennen kuin käytät kontekstia:
- Aloita välittämällä propsit. Jos komponenttisi eivät ole triviaaleja, ei ole harvinaista että sinun pitää välittää kymmeniä propseja useita tasoja syvälle. Se saattaa tuntua työläältä, mutta se tekee selväksi, mitkä komponentit käyttävät mitäkin dataa! Henkilö joka ylläpitää koodiasi on kiitollinen, että olet tehnyt datan virtauksen selkeäksi propseilla.
- Erota komponentit ja välitä JSX
children
:nä niille. Jos välität jotain dataa useiden komponenttien läpi, jotka eivät käytä tätä dataa (ja vain välittävät sen vain eteenpäin), tämä usein tarkoittaa että olet unohtanut erottaa joitain komponentteja matkan varrella. Esimerkiksi, ehkä välität data-propseja kutenposts
visuaalisille komponenteille jotka eivät käytä niitä suoraan, kuten<Layout posts={posts} />
. Sen sijaan, muokkaaLayout
ottamaanchildren
propsina ja renderöi<Layout><Posts posts={posts} /></Layout>
. Tämä vähentää tasojen määrää dataa välittävien komponenttien ja dataa käyttävien komponenttien välillä.
Jos nämä eivät toimi, harkitse kontekstia.
Contextin käyttökohteita
- Teemat: Jos sovelluksesi antaa käyttäjän vaihtaa sen ulkoasua (esim. tumma tila), voit laittaa kontekstitarjoajan ylätasolle, ja käyttää sitä komponenteissa joiden täytyy muuttaa ulkoasuaan.
- Tämänhetkinen tili: Monet komponentit saattavat tarvita tietää tämän hetkinen sisäänkirjautunut käyttäjä. Sen laittaminen kontekstiin tekee sen lukemisesta helppoa mistä tahansa puun tasosta. Joissain sovelluksissa voit myös toimia useiden tilien kanssa samanaikaisesti (esim. jättää kommentin eri käyttäjänä). Tällöin voi olla kätevää kääriä osa käyttöliittymästä sisäkkäiseen tarjoajaan eri tiliarvolla.
- Reititys: Useimmat reititysratkaisut käyttävät kontekstia sisäisesti pitääkseen kirjaa nykyisestä reitistä. Tämä on se tapa jolla jokainen linkki “tietää” onko se aktiivinen vai ei. Jos rakennat oman reititysratkaisusi, saatat haluta tehdä samoin.
- Tilan hallinta: Kun sovelluksesi kasvaa, saatat joutua pitämään paljon tilaa ylhäällä puussa. Monet kaukaiset komponentit alhaalla saattavat haluta muuttaa sitä. Yleensä käytetään reduktoria yhdessä kontekstin kanssa hallitaksesi monimutkaista tilaa ja välittääksesi sen eteenpäin komponenteille, jotka ovat kaukana toisistaan.
Konteksti ei ole rajoitettu staattisiin arvoihin. Jos välität eri arvon seuraavalla renderöinnillä, React päivittää kaikki komponentit jotka lukevat sitä alapuolelta! Tämä on syy miksi kontekstia käytetään usein yhdistettynä tilaan.
Yleisesti, jos jokin tieto on tarpeen kaukaisissa komponenteissa puussa, se on hyvä merkki siitä että konteksti auttaa sinua.
Kertaus
- Kontekstin avulla komponentti voi tarjota tietoa koko puun alapuolelle.
- Välittääksesi konteksti:
- Luo ja exporttaa se koodilla
export const MyContext = createContext(defaultValue)
. - Välitä se
useContext(MyContext)
hookille luettavaksi minkä tahansa lapsikomponentin sisällä, riippumatta siitä kuinka syvälle se on. - Kääri lapsikomponentit
<MyContext.Provider value={...}>
-komponenttiin tarjotaksesi sen yläpuolelta.
- Luo ja exporttaa se koodilla
- Konteksti välittyy välissä olevien komponenttien läpi.
- Konteksti antaa sinun kirjoittaa komponentteja jotka “sovittuvat ympäristöönsä”.
- Ennen kuin käytät kontekstia, yritä välittää propseja tai välitä JSX
children
:nä.
Haaste 1 / 1: Korvaa propsien välittäminen contextilla
Tässä esimerkissä, valintaruudun tilan vaihtaminen muuttaa imageSize
propsia joka välitetään kaikille <PlaceImage>
-komponenteille. Valintaruudun tila pidetään ylätason App
-komponentissa, mutta jokainen <PlaceImage>
-komponentti tarvitsee sen tiedon.
Tällä hetkellä, App
välittää imageSize
propin List
:lle, joka välittää sen jokaiselle Place
:lle, joka välittää sen PlaceImage
:lle. Poista imageSize
propsi, ja välitä se suoraan App
-komponentista PlaceImage
:lle.
Voit määritellä kontekstin Context.js
-tiedostossa.
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} /> ); }