Mengoper Data Secara Mendalam dengan Context
Biasanya, Anda akan mengoper informasi dari komponen induk ke komponen anak melalui props. Tapi mengoper props bisa menjadi bertele-tele dan merepotkan jika Anda harus mengopernya melalui banyak komponen di tengah-tengah, atau jika banyak komponen di aplikasi Anda membutuhkan informasi yang sama. Context memungkinkan komponen induk untuk membuat beberapa informasi tersedia di komponen lain di pohon (tree) di bawahnya—tidak peduli seberapa dalam—tanpa mengopernya secara eksplisit melalui props.
Anda akan mempelajari
- Apa itu ”prop drilling”
- Bagaimana cara mengganti pengiriman props yang berulang dengan context
- Kasus umum untuk penggunaan context
- Alternatif umum untuk context
Masalah ketika mengoper props
Mengoper props adalah cara yang bagus untuk menyalurkan data secara eksplisit melalui pohon (tree) UI Anda ke komponen yang menggunakanya.
Tapi mengoper props bisa menjadi bertele-tele dan tidak nyaman ketika Anda perlu mengoper beberapa prop secara mendalam melalui pohon (tree), atau jika banyak komponen membutuhkan prop yang sama. Leluhur umum terdekat bisa jadi jauh dari komponen yang membutuhkan data, dan memindahkan state ke atas dapat menyebabkan yang disebut ”prop drilling“.
Bukankah lebih bagus jika ada cara untuk “memindahkan” data ke komponen di dalam pohon (tree) yang membutuhkannya tanpa harus mengoper props? Dengan fitur context React, ternyata ada!
Context: sebuah alternatif untuk mengoper props
Context memungkinkan sebuah komponen induk menyediakan data untuk seluruh pohon (tree) di bawahnya. Ada banyak kegunaan dari context. Berikut ini salah satu contohnya. Perhatikan komponen Heading
ini yang menerima level
untuk ukurannya:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Judul</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> ); }
Katakanlah Anda ingin beberapa judul dalam Section
yang sama selalu memiliki ukuran yang sama:
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> ); }
Saat ini, Anda mengoper level
props ke tiap <Heading>
secara terpisah:
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
Akan lebih baik jika Anda dapat mengoper prop level
ke komponen <Section>
dan menghapusnya dari komponen <Heading>
Dengan cara ini Anda dapat menerapkan bahwa semua judul di bagian yang sama memiliki ukuran yang sama:
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
Tapi bagaimana komponen <Heading>
dapat mengetahui level <Section>
yang terdekat? Hal ini akan membutuhkan suatu cara untuk “meminta” data dari suatu tempat di atas pohon (tree).
Anda tidak bisa melakukannya dengan props sendirian. Di sinilah context berperan penting. Anda akan melakukannya dalam tiga langkah:
- Buat sebuah context. (Anda dapat menamainya
LevelContext
, karena ini untuk level judul.) - Gunakan context tersebut dari komponen yang membutuhkan data. (
Heading
akan menggunakanLevelContext
.) - Sediakan context tersebut dari komponen yang menentukan data. (
Section
akan menyediakanLevelContext
.)
Context memungkinkan sebuah induk—bahkan yang jauh sekalipun!—menyediakan beberapa data kepada seluruh komponen pohon (tree) di dalamnya.
Langkah 1: Buat context
Pertama, Anda perlu membuat context. Anda harus mengekspornya dari sebuah file sehingga komponen Anda dapat menggunakannya:
import { createContext } from 'react'; export const LevelContext = createContext(1);
Satu-satunya argumen untuk createContext
adalah nilai default. Disini, 1
merujuk pada level heading terbesar, tapi Anda dapat mengoper nilai apa pun (bahkan sebuah objek). Anda akan melihat pentingnya nilai default di langkah selanjutnya.
Langkah 2: Gunakan context
Impor useContext
Hook dari React dan context Anda:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
Saat ini, komponen Heading
membaca level
dari props:
export default function Heading({ level, children }) {
// ...
}
Sebagai gantinya, hapus prop level
dan baca nilai dari context yang baru saja Anda impor, LevelContext
:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
adalah sebuah Hook. Sama seperti useState
dan useReducer
, Anda hanya dapat memanggil sebuah Hook secara langsung di dalam komponen React (bukan di dalam pengulangan atau pengkondisian). useContext
memberitahu React bahwa komponen Heading
mau membaca LevelContext
.
Sekarang komponen Heading
tidak membutuhkan sebuah prop level
, Anda tidak perlu mengoper level prop ke Heading
di JSX Anda seperti ini lagi:
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
Sebagai gantinya Perbarui JSX sehingga Section
yang dapat menerimanya:
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
Sebagai pengingat, ini adalah markup yang sedang Anda coba untuk bekerja:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Judul</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> ); }
Perhatikan contoh ini masih belum berfungsi dengan baik! Semua judul memiliki ukuran yang sama karena meskipun Anda menggunakan context, Anda belum menyediakannya. React tidak tahu darimana untuk mendapatkannya!
Jika Anda tidak menyediakan context, React akan menggunakan nilai default yang sudah Anda tentukan di langkah sebelumnya. Di contoh ini, Anda menentukan 1
sebagai argumen createContext
, jadi useContext(LevelContext)
mengembalikan 1
, mengatur semua headings ke <h1>
. Ayo kita perbaiki masalah ini dengan membuat setiap Section
menyediakan context-nya sendiri.
Langkah 3: Sediakan context
Komponen Section
saat ini merenders anaknya:
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
Bungkus mereka semua dengan sebuah context provider untuk menyediakan LevelContext
kepada mereka:
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
Ini memberitahu React: “jika ada komponen di dalam <Section>
ini yang meminta LevelContext
, berikan level
ini.” Komponen akan menggunakan nilai dari <LevelContext.Provider>
terdekat di pohon UI (tree) di atasnya.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Judul</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> ); }
Hasilnya sama dengan kode aslinya, tapi Anda tidak perlu mengoper prop level
ke setiap komponen Heading
! Sebagai gantinya, ia “mencari tahu” level heading-nya dengan meminta Section
terdekat di atasnya:
- Anda mengoper prop
level
ke<Section>
. Section
membungkus anaknya dengan<LevelContext.Provider value={level}>
.Heading
meminta nilai terdekat dariLevelContext
di atasnya denganuseContext(LevelContext)
.
Menggunakan dan menyediakan context dari komponen yang sama
Saat ini, Anda masih harus menentukan setiap level
section’s secara manual:
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
Karena context memungkinan Anda membaca informasi dari komponen di atasnya, setiap Section
dapat membaca level
dari Section
di atasnya, dan mengoper level + 1
ke bawah secara otomatis. Berikut adalah bagaimana Anda dapat melakukannya:
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>
);
}
Dengan perubahan ini, Anda tidak perlu mengoper prop level
baik ke <Section>
atau ke <Heading>
:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Judul</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> ); }
Sekarang keduanya Heading
dan Section
membaca LevelContext
untuk mencari tahu seberapa “dalam” mereka. Dan Section
membungkus anaknya ke dalam LevelContext
untuk menentukan bahwa apa pun yang ada di dalamnya berada pada level yang “lebih dalam”.
Context melewati komponen perantara
Anda dapat menyisipkan sebanyak mungkin komponen di antara komponen yang menyediakan context dan komponen yang menggunakannya. Ini termasuk komponen bawaan seperti <div>
dan komponen yang mungkin Anda buat sendiri.
Di contoh berikut, komponen Post
yang sama (dengan batas putus-putus) diberikan pada dua tingkat sarang yang berbeda. Perhatikan bahwa <Heading>
di dalamnya mendapatkan level-nya secara otomatis dari <Section>
terdekat:
import Heading from './Heading.js'; import Section from './Section.js'; export default function ProfilePage() { return ( <Section> <Heading>Profil Saya</Heading> <Post title="Hello traveller!" body="Baca tentang petualangan Saya." /> <AllPosts /> </Section> ); } function AllPosts() { return ( <Section> <Heading>Posts</Heading> <RecentPosts /> </Section> ); } function RecentPosts() { return ( <Section> <Heading>Posting Terbaru</Heading> <Post title="Cita Rasa Lisbon" body="...those pastéis de nata!" /> <Post title="Buenos Aires dalam irama tango" body="Saya menyukainya!" /> </Section> ); } function Post({ title, body }) { return ( <Section isFancy={true}> <Heading> {title} </Heading> <p><i>{body}</i></p> </Section> ); }
Anda tidak perlu melakukan sesuatu yang khusus untuk pekerjaan ini. Section
menentukan context untuk pohon (tree) di dalamnya, jadi Anda dapat menyisipkan <Heading>
di mana saja, dan akan memiliki ukuran yang benar. Cobalah di kotak pasir di atas!
Context memungkinkan Anda untuk menulis komponen yang “beradaptasi dengan sekitar mereka” dan menampilkan diri mereka secara berbeda tergantung di mana (atau, dalam kata lain, dalam context apa) mereka akan diberikan.
Cara kerja context mungkin mengingatkan Anda pada pewarisan properti CSS. Pada CSS, Anda dapat menentukan color: blue
untuk <div>
, dan simpul DOM apa pun di dalamnya, tidak peduli seberapa dalam, akan mewarisi warna tersebut kecuali ada simpul DOM lain di tengahnya yang menimpanya dengan color: green
. Demikian pula, dalam React, satu-satunya cara untuk menimpa beberapa context yang berasal dari atas adalah dengan membungkus anaknya ke dalam penyedia context dengan nilai yang berbeda.
Pada CSS, properti berbeda seperti color
dan background-color
tidak akan menimpa satu sama lain. Anda dapat mengatur semua color
<div>
ke merah tanpa berdampak pada background-color
. Demikian pula, React contexts yang berbeda tidak akan menimpa satu sama lain. Tiap context yang Anda buat dengan createContext()
benar-benar terpisah dari yang lain, dan menyatukan komponen-komponen yang menggunakan dan menyediakan context tertentu. Satu komponen dapat menggunakan atau menyediakan banyak konteks yang berbeda tanpa masalah.
Sebelum Anda menggunakan context
Context sangat menggoda untuk digunakan! Namun, ini juga terlalu mudah untuk menggunakannya secara berlebihan. Hanya karena Anda membutuhkan untuk mengoper beberapa props beberapa tingkat lebih dalam bukan berarti Anda memasukkan informasi tersebut ke dalam context.
Ini adalah beberapa alternatif yang harus Anda pertimbangkan sebelum menggunakan context:
- Mulai dengan mengoper props. Jika komponen Anda tidak sepele, bukan hal yang aneh jika Anda mengoper selusin props melalui selusin komponen. Ini mungkin terasa seperti pekerjaan berat, tapi membuatnya sangat jelas komponen yang mana menggunakan data yang mana! Orang yang mengelola kode Anda akan senang Anda telah membuat aliran data eksplisit dengan props.
- Ekstrak komponen dan oper JSX sebagai
children
ke mereka. Jika Anda mengoper beberapa data melewati banyak lapisan komponen perantara yang tidak menggunakan data tersebut (dan hanya mengopernya lebih jauh ke bawah), ini sering kali Anda lupa mengekstrak beberapa komponen di sepanjang jalan. Contohnya, mungkin Anda mengoper data props sepertiposts
ke komponen visual yang tidak menggunakannya secara langsung, seperti<Layout posts={posts} />
. Sebagai gantinya, buatLayout
mengambilchildren
sebagai prop, dan berikan<Layout><Posts posts={posts} /></Layout>
. Hal ini mengurangi jumlah lapisan antara komponen yang menentukan data dan komponen yang membutuhkannya.
Jika tidak satu pun dari pendekatan ini yang cocok untuk Anda, pertimbangkan context.
Kasus penggunakan untuk context
- Tema: Jika aplikasi Anda memungkinkan pengguna mengganti penampilannya (misalnya mode gelap), Anda dapat menempatkan penyedia context di paling atas aplikasi Anda, dan menggunakan konteksnya di komponen yang membutuhkan untuk menyesuaikan tampilan visual mereka.
- Akun saat ini: Banyak komponen yang mungkin perlu mengetahui pengguna yang sedang masuk. Menempatkannya dalam konteks akan memudahkan untuk membacanya di mana saja di dalam pohon (tree). Beberapa aplikasi memungkinkan Anda mengoperasikan beberapa akun pada saat yang sama (misalnya untuk memberikan komentar sebagai pengguna yang berbeda). Dalam kasus tersebut, akan lebih mudah untuk membungkus bagian dari UI ke dalam penyedia bersarang dengan nilai akun saat ini yang berbeda.
- Routing: Sebagian besar solusi routing menggunakan context secara internal untuk menyimpan rute saat ini. Dengan cara inilah setiap link “mengetahui” apakah ia aktif atau tidak. Jika Anda membuat router anda sendiri, Anda mungkin ingin melakukannya juga.
- Mengelola state: Seiring pertumbuhan aplikasi Anda, Anda mungkin akan menempatkan banyak state yang lebih dekat ke bagian atas aplikasi Anda. Banyak komponen yang jauh di bawahnya mungkin ingin untuk mengubahnya. Ini adalah hal yang umum untuk menggunakan reducer bersama dengan context untuk mengelola state yang kompleks dan mengopernya ke komponen yang jauh ke bawah tanpa terlalu banyak kerumitan.
Context tidak terbatas pada nilai statis. Jika anda memberikan nilai yang berbeda pada render berikutnya, React akan memperbarui semua komponen yang membacanya di bawahnya! Inilah sebabnya mengapa context sering digunakan bersama degan state.
Pada umumnya, jika beberapa informasi dibutuhkan oleh komponen yang jauh di beberapa bagian pohon (tree), ini adalah indikasi yang bagus bahwa context akan membantu Anda.
Rekap
- Context memungkinkan komponen menyediakan beberapa informasi ke keseluruhan pohon (tree) di bawahnya.
- Untuk mengoper context:
- Buat dan ekspor ia dengan
export const MyContext = createContext(defaultValue)
. - Oper ke
useContext(MyContext)
Hook untuk membacanya di komponen anak manapun, tidak peduli seberapa dalam. - Bungkus anak ke
<MyContext.Provider value={...}>
untuk menyediakannya dari induk.
- Buat dan ekspor ia dengan
- Context melewati komponen apa pun di tengahnya.
- Context memungkinkan Anda menulis komponen yang “beradaptasi dengan sekitar mereke”.
- Sebelum Anda menggunakan context, coba oper props atau oper JSX sebagai
children
.
Tantangan 1 dari 1: Ganti prop drilling dengan context
Pada contoh ini, mengganti checkbox akan mengubah imageSize
prop yang diteruskan ke setiap <PlaceImage>
. checkbox state akan disimpan di komponen paling atas komponen App
, tapi tiap <PlaceImage>
harus menyadarinya.
Saat ini, App
mengoper imageSize
ke List
, yang mana mengopernya ke tiap Place
, yang mana mengopernya ke tiap PlaceImage
. Hapus prop imageSize
, dan sebagai gantinya oper ia dari komponen App
ke PlaceImage
secara langsung.
Anda dapat deklarasi context di 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); }} /> Menggunakan gambar besar </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} /> ); }