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).

Tilan nostaminen ylös

Kaavio kolmesta komponentista. Ylin komponentti sisältää violetin värisen pallon, joka edustaa arvoa. Arvo virtaa alakomponentteihin, jotka ovat molemmat violetin värisiä.
Kaavio kolmesta komponentista. Ylin komponentti sisältää violetin värisen pallon, joka edustaa arvoa. Arvo virtaa alakomponentteihin, jotka ovat molemmat violetin värisiä.

Propsien poraus

Kaavio kymmenestä solmusta koostuvasta puusta, jossa jokaisella solmulla on kaksi tai vähemmän lasta. Juurisolmu sisältää violetin värisen pallon, joka edustaa arvoa. Arvo virtaa alakomponentteihin, kunkin niistä välittäen sen eteenpäin, mutta ei sisältäen sitä. Vasemmanpuoleinen lapsi välittää arvon kahden lapsen kautta, jotka ovat molemmat violetin värisiä. Oikeanpuoleinen alakomponentti välittää arvon yhdelle sen kahdesta lapsesta - oikealle lapselle, joka on violetin värisenä. Tämä lapsi välittää arvon yhdelle lapselleen, joka välittää sen kahden lapsen kautta, jotka ovat molemmat violetin värisiä.
Kaavio kymmenestä solmusta koostuvasta puusta, jossa jokaisella solmulla on kaksi tai vähemmän lasta. Juurisolmu sisältää violetin värisen pallon, joka edustaa arvoa. Arvo virtaa alakomponentteihin, kunkin niistä välittäen sen eteenpäin, mutta ei sisältäen sitä. Vasemmanpuoleinen lapsi välittää arvon kahden lapsen kautta, jotka ovat molemmat violetin värisiä. Oikeanpuoleinen alakomponentti välittää arvon yhdelle sen kahdesta lapsesta - oikealle lapselle, joka on violetin värisenä. Tämä lapsi välittää arvon yhdelle lapselleen, joka välittää sen kahden lapsen kautta, jotka ovat molemmat violetin värisiä.

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:

  1. Luo konteksti. (Voit kutsua sitä LevelContext:ksi, koska se on otsiko tasoa varten.)
  2. Käytä kontekstia komponentista, joka tarvitsee datan. (Heading käyttää LevelContext:a.)
  3. Tarjoa konteksti komponentista, joka määrittää datan. (Section tarjoaa LevelContext:n.)

Konteksti antaa pääkomponentin - jopa kaukaisen - tarjota joitain tietoja koko puun sisällä.

Kontekstin käyttö lähellä olevissa lapsikomponenteissa

Kaavio kolmesta komponentista. Pääkomponentti sisältää kuplan, joka edustaa oranssilla korostettua arvoa, joka projisoi alas kahden lapsen, jotka ovat molemmat korostettu oranssilla.
Kaavio kolmesta komponentista. Pääkomponentti sisältää kuplan, joka edustaa oranssilla korostettua arvoa, joka projisoi alas kahden lapsen, jotka ovat molemmat korostettu oranssilla.

Kontekstin käyttö kaukaisissa lapsikomponenteissa

Kaavio kymmenestä solmusta koostuvasta puusta, jokaisella solmulla on kaksi tai vähemmän lasta. Juurisolmu sisältää kuplan, joka edustaa oranssilla korostettua arvoa, joka projisoi alas suoraan neljään lehteen ja yhteen välikomponenttiin puussa, jotka ovat kaikki korostettu oranssilla. Muita välikomponentteja ei ole korostettu.
Kaavio kymmenestä solmusta koostuvasta puusta, jokaisella solmulla on kaksi tai vähemmän lasta. Juurisolmu sisältää kuplan, joka edustaa oranssilla korostettua arvoa, joka projisoi alas suoraan neljään lehteen ja yhteen välikomponenttiin puussa, jotka ovat kaikki korostettu oranssilla. Muita välikomponentteja ei ole korostettu.

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:

  1. Välität level -propsin <Section> -komponentille.
  2. Section käärii lapsensa <LevelContext.Provider value={level}>:n sisään.
  3. Heading kysyy lähimmän LevelContext:n arvon useContext(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.

Huomaa

Tämä esimerkki käyttää otsikkotasojen määrittämistä koska se näyttää visuaalisesti kuinka sisäkkäiset komponentit voivat ohittaa kontekstin. Mutta konteksti on hyödyllinen monille muillekin käyttötarkoituksille. Voit käyttää sitä välittämään tietoa, jota koko alipuu tarvitsee: nykyinen väriteema, tällä hetkellä kirjautunut käyttäjä, jne.

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:

  1. 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.
  2. 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 kuten posts visuaalisille komponenteille jotka eivät käytä niitä suoraan, kuten <Layout posts={posts} />. Sen sijaan, muokkaa Layout ottamaan children 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:
    1. Luo ja exporttaa se koodilla export const MyContext = createContext(defaultValue).
    2. Välitä se useContext(MyContext) hookille luettavaksi minkä tahansa lapsikomponentin sisällä, riippumatta siitä kuinka syvälle se on.
    3. Kääri lapsikomponentit <MyContext.Provider value={...}>-komponenttiin tarjotaksesi sen yläpuolelta.
  • 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}
    />
  );
}