Context ile Veriyi Derinlemesine Aktarma
Bilgiyi genelde prop’lar vasıtasıyla üst elemandan alt elemana doğru aktarırsınız. Ancak, aktarmanız gereken bileşen ulaşana kadar birçok ara bileşene iletmeniz veya birden çok bileşene aktarmanız gerekiyorsa prop kullanmak zahmetli ve karmaşık hale gelir. Context, bilgiyi üst bileşenden ihtiyaç duyan alt bileşenlere (derinliğine bakılmaksızın) prop olarak açıkça belirtmeden iletmenizi sağlar.
Bunları öğreneceksiniz
- ”Prop drilling” nedir
- Birden fazla kez alt elemana aktarılan prop’u context ile değiştirmek
- Context’in yaygın kullanım durumları
- Context’in yaygın alternatifleri
Prop’ları aktarmanın yarattığı sorun
Prop’ları aktarmak, UI ağacınızdaki bileşenlere kullanacağı verileri iletmenin harika bir yoludur.
Ancak, bir prop’u ağacın derinliklerine aktarmak gerektiğinde veya birçok elemanın aynı prop’a ihtiyaç duyduğu durumlarda zahmetli ve uygunsuz hale gelebilir. Veriye ihtiyaç duyan elemanlar ile en yakın ortak üst bileşen arasındaki mesafe uzun olabilir ve state’i yukarı taşımak “prop drilling” adı verilen duruma yol açabilir.
Veriyi prop’lar ile aktarmadan ağaçtaki bileşenlere “ışınlamanın” bir yolu olsa harika olmaz mıydı? React’ın context özelliği sayesinde bu mümkün!
Context: prop’ları aktarmanın alternatif bir yolu
Context, üst bileşenin altındaki tüm ağaca veri sağlamasına olanak tanır. Bir çok kullanım alanı vardır. İşte bir örnek. Boyutu için level
kabul eden bu Heading
elemanını ele alalım:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Üst Başlık</Heading> <Heading level={2}>Başlık</Heading> <Heading level={3}>Alt-başlık</Heading> <Heading level={4}>Alt-alt-başlık</Heading> <Heading level={5}>Alt-alt-alt-başlık</Heading> <Heading level={6}>Alt-alt-alt-alt-başlık</Heading> </Section> ); }
Diyelim ki aynı Section
içerisinde birden fazla başlığın her zaman aynı boyutta olmasını istiyorsunuz:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Üst Başlık</Heading> <Section> <Heading level={2}>Başlık</Heading> <Heading level={2}>Başlık</Heading> <Heading level={2}>Başlık</Heading> <Section> <Heading level={3}>Alt-başlık</Heading> <Heading level={3}>Alt-başlık</Heading> <Heading level={3}>Alt-başlık</Heading> <Section> <Heading level={4}>Alt-alt-başlık</Heading> <Heading level={4}>Alt-alt-başlık</Heading> <Heading level={4}>Alt-alt-başlık</Heading> </Section> </Section> </Section> </Section> ); }
Şu ana kadar, level
prop’unu her <Heading>
için tek tek tanımladınız:
<Section>
<Heading level={3}>Hakkında</Heading>
<Heading level={3}>Fotoğraflar</Heading>
<Heading level={3}>Videolar</Heading>
</Section>
Bunun yerine level
prop’unu <Section>
bileşenine aktarıp <Heading>
’den kaldırabilseydiniz daha iyi olurdu. Böylece aynı bölümdeki tüm başlıkların aynı boyuta sahip olmasını sağlayabilirsiniz:
<Section level={3}>
<Heading>Hakkında</Heading>
<Heading>Fotoğraflar</Heading>
<Heading>Videolar</Heading>
</Section>
Peki <Heading>
elemanı kendine en yakın <Section>
elemanının seviyesini nasıl bilebilir? Bunun için alt bileşenin yukarıdaki bir yerden veri “istemesi” gerekir.
Bunu sadece prop’lar ile yapamazsınız. Context burada devreye girer. Bunu üç adımda yaparsınız:
- Context oluşturun. (Başlık seviyesi için olduğundan
LevelContext
olarak isimlendirebilirsiniz.) - Context’i veriye ihtiyacı olan bileşende kullanın. (
Heading
,LevelContext
’i kullanacak.) - Veriyi tanımlayacak bileşenden context’i sağlayın. (
Section
,LevelContext
’i sağlayacak.)
Context, üst bileşenin—uzakta olsa bile!—içindeki tüm ağaca veri aktarmasını sağlar.
Adım 1: Context’i oluşturun
Öncelikle context’i oluşturmanız gerekir. Bileşenlerinizin kullanabilmesi için bunu bir dosyadan dışa aktarmalısınız:
import { createContext } from 'react'; export const LevelContext = createContext(1);
createContext
’e verilen tek argüman varsayılan değeridir. Burada, 1
en büyük başlık seviyesine karşılık gelir ancak herhangi bir değer (hatta obje) verebilirsiniz. Varsayılan değerin önemini bir sonraki adımda göreceksiniz.
Adım 2: Context’i kullanın
React’tın useContext
hook’unu ve context’inizi içe aktarın:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
Şuanda, Heading
elemanı level
’ı prop’lardan okur:
export default function Heading({ level, children }) {
// ...
}
Bunu yerine, level
prop’unu kaldırın ve değeri az önce içe aktardığınız context’ten (LevelContext
) okuyun:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
bir Hook’tur. Tıpkı useState
ve useReducer
gibi, yalnızca React bileşeninin üst kapsamında çağırabilirsiniz (döngülerin veya koşulların içinde çağıramazsınız). useContext
, React’e Heading
bileşeninin LevelContext
’i okumak istediğini söyler.
Artık Heading
bileşeninin level
prop’u olmadığına göre Heading
’e aşağıdaki JSX’te olduğu gibi aktarmanıza da gerek yoktur:
<Section>
<Heading level={4}>Alt-alt-başlık</Heading>
<Heading level={4}>Alt-alt-başlık</Heading>
<Heading level={4}>Alt-alt-başlık</Heading>
</Section>
Bunun yerine JSX’i Section
’ın level
prop’unu alacağı şekilde güncelleyin:
<Section level={4}>
<Heading>Alt-alt-başlık</Heading>
<Heading>Alt-alt-başlık</Heading>
<Heading>Alt-alt-başlık</Heading>
</Section>
Hatırlatmak gerekirse, yapmaya çalıştığınız biçimlendirme budur:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Üst Başlık</Heading> <Section level={2}> <Heading>Başlık</Heading> <Heading>Başlık</Heading> <Heading>Başlık</Heading> <Section level={3}> <Heading>Alt-başlık</Heading> <Heading>Alt-başlık</Heading> <Heading>Alt-başlık</Heading> <Section level={4}> <Heading>Alt-alt-başlık</Heading> <Heading>Alt-alt-başlık</Heading> <Heading>Alt-alt-başlık</Heading> </Section> </Section> </Section> </Section> ); }
Bu örneğin henüz tam olarak çalışmadığına dikkat edin! Tüm başlıklar aynı boyuta sahip. Context’i kullanıyorsunuz ancak henüz sağlamadınız. React nereden alacağını bilmiyor!
Context’i sağlamazsanız, React önceki adımda belirttiğiniz varsayılan değeri kullanır. Bu örnekte, createContext
’e argüman olarak 1
belirttiniz. Bu nedenle useContext(LevelContext)
ifadesi 1
döndürür ve tüm bu başlıkları <h1>
olarak ayarlar. Her Section
’ın kendi context’ini sağlamasını ayarlayarak bu sorunu çözelim.
Adım 3: Context’i sağlayın
Section
bileşeni şu anda alt bileşenlerini render eder:
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
Alt bileşenlerine LevelContext
sağlamak için context provider ile sarın:
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
Bu React’a şunu söyler: “<Section>
içindeki herhangi bir eleman,LevelContext
’i istediğinde, ona bu level
değerini ver.” Bileşen, üzerindeki UI ağacında bulunan en yakın <LevelContext.Provider>
değerini kullanır.
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>Başlık</Heading> <Heading>Başlık</Heading> <Heading>Başlık</Heading> <Section level={3}> <Heading>Alt-başlık</Heading> <Heading>Alt-başlık</Heading> <Heading>Alt-başlık</Heading> <Section level={4}> <Heading>Alt-alt-başlık</Heading> <Heading>Alt-alt-başlık</Heading> <Heading>Alt-alt-başlık</Heading> </Section> </Section> </Section> </Section> ); }
Orijinal kodla aynı sonucu elde edersiniz, ancak her Heading
bileşenine level
prop’unu aktarmanız gerekmez! Bunun yerine, üstündeki en yakın Section
bileşenine sorarak başlık seviyesini “bulur”:
level
prop’unu<Section>
’a aktarırsınız.Section
alt bileşenlerini<LevelContext.Provider value={level}>
sarmalar.Heading
,useContext(LevelContext)
ile birlikte yukarıdaki en yakınLevelContext
’e değerini sorar.
Context değerini provider’ının tanımlandığı bileşende okuma
Şu anda hala her bölümün level
’ını manuel olarak belirlemeniz gerekir:
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
Context, üstteki bileşenlerden bilgi okumanıza izin verdiğinden, her Section
üstündeki Section
’dan level
değerini okuyarak level + 1
değerini otomatik olarak aşağıya aktarabilir. Bunu nasıl yapabileceğinize dair bir örnek:
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>
);
}
Bu değişiklik ile birlikte artık level
prop’unu ne <Section>
’a ne de <Heading>
’ e aktarmanıza gerek kalmaz:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Üst Başlık</Heading> <Section> <Heading>Başlık</Heading> <Heading>Başlık</Heading> <Heading>Başlık</Heading> <Section> <Heading>Alt-başlık</Heading> <Heading>Alt-başlık</Heading> <Heading>Alt-başlık</Heading> <Section> <Heading>Alt-alt-başlık</Heading> <Heading>Alt-alt-başlık</Heading> <Heading>Alt-alt-başlık</Heading> </Section> </Section> </Section> </Section> ); }
Şimdi hem Heading
hem de Section
LevelContext
değerini okuyarak ne kadar “derinde” olduklarını anlayabilir. Ve Section
içinde bulunan her şeyin daha “derin” bir seviyede olduğunu belirtmek için çocuk elemanlarını LevelContext
ile sarıyor.
Context’in ara bileşenlerden aktarılması
Context’i sağlayan ve kullanan bileşenlerin arasına istediğiniz kadar bileşen ekleyebilirsiniz. Bu, hem <div>
gibi yerleşik bileşenleri hem de kendi bileşenleriniz olabilir.
Bu örnekte, Post
elemanı (çizgili çerçevesi olan) iki farklı derinlikte render ediliyor. Dikkat ederseniz içindeki <Heading>
elemanı boyutunu otomatik olarak kendisine en yakın olan <Section>
’dan alıyor:
Bu örnekte, aynı Post
bileşeni (kesikli kenarlıklı) iki farklı derinlikte render edilmiştir. İçindeki <Heading>
’in seviyesini otomatik olarak en yakın <Section>
’dan aldığına dikkat edin:
import Heading from './Heading.js'; import Section from './Section.js'; export default function ProfilePage() { return ( <Section> <Heading>Profilim</Heading> <Post title="Merhaba gezgin!" body="Maceralarımı oku." /> <AllPosts /> </Section> ); } function AllPosts() { return ( <Section> <Heading>Posts</Heading> <RecentPosts /> </Section> ); } function RecentPosts() { return ( <Section> <Heading>Son Yazılarım</Heading> <Post title="Lizbon'un lezzetleri" body="Enfes Portekiz tatlısı!" /> <Post title="Tango ritminde Buenos Aires" body="Bayıldım!" /> </Section> ); } function Post({ title, body }) { return ( <Section isFancy={true}> <Heading> {title} </Heading> <p><i>{body}</i></p> </Section> ); }
Bunun çalışması için herhangi özel bir şey yapmadınız. Section
içinde bulunduğu ağaç için context’i belirler. <Heading>
’i istediğiniz yerde kullanabilirsiniz ve her zaman doğru boyut da olacaktır. Yukarıdaki sandbox’ta deneyin!
Context “çevresine adapte olan” ve nerede (başka bir deyişle hangi context’te) render edildiklerine bağlı olarak farklı şekilde gözüken bileşenler yazmanıza olanak sağlar.
Context’in çalışma şekli size CSS özellik kalıtımını andırabilir. CSS’de bir <div>
için color: blue
belirttiğinizde içindeki herhangi bir DOM düğümü color: green
ile ezmediği sürece tüm elemanlar bu rengi kalıtır. Benzer şekilde, React’te yukarıdan gelen context’i ezmenin tek yolu alt bileşeni farklı bir değere sahip bir context provider’a sarmalamaktır.
CSS kullanırken, birbirinden farklı özellikler mesela, color
ve background-color
birbirini geçersiz kılmaz. Bütün bir <div>
’in color
özelliğini background-color
’ı etkilemeden değiştirebilirsiniz. Aynı şekilde, farklı React context’leri birbirini geçersiz kılmaz. createContext()
ile yarattığınız bütün context’ler birbirinden tamamen ayrıdır, ve her biri kendi context’i ile o context’i kullanan elemanları etkiler. Bir eleman birden çok context kullanabilir ve sağlayabilir, bu bir sorun teşkil etmez.
CSS’de color
ve background-color
gibi farklı özellikler birbirini ezmez. Arka plan rengini etkilemeden tüm <div>
’lerin metin rengini kırmızı olarak ayarlayabilirsiniz. Benzer şekilde, farklı React context’leri birbirini ezmez. createContext()
ile oluşturduğunuz her context diğerlerinden tamamen ayrıdır ve o context’i kullanan ve sağlayan bileşenleri birbirine bağlar. Bileşenler birden fazla farklı context’i sorunsuzca kullanabilir.
Context’i kullanmadan önce
Context’i kullanmak çok caziptir! Bu yüzden, gereksiz ve fazla kullanabilirsiniz. Prop’ları birkaç eleman derine indirmeniz gerekiyorsa, bu context kullanmalısınız anlamına gelmez. Context’i kullanmak çok caziptir! Ancak, bu aynı zamanda gereğinden fazla kullanmanın da çok kolay olduğu anlamına gelir. Bazı prop’ları birkaç seviye derine aktarmanızın gerekmesi, bu bilgiler için context kullanmanız gerektiği anlamına gelmez.
Context kullanmadan önce düşünmeniz için bir kaç alternatif:
- Prop olarak aktararak başlayın. Eğer küçük bileşenleriniz yoksa, bir düzine bileşen için bir düzine prop aktarmak olağandışı bir durum değildir. Zahmetli gibi görünebilir ancak hangi bileşenlerin hangi veriyi kullandığını çok net bir şekilde gösterir! Kodunuzun bakımını yapan kişi, veri akışını prop’lar ile açık bir şekilde belirttiğiniz için size minnettar olacaktır.
- Bileşenlere ayırın ve JSX’i
children
olarak aktarın. Bazı verileri, bu veriyi kullanmayan (yalnızca aşağıya aktaran) birçok ara bileşen katmanından geçirmeniz gerekiyorsa, genellikle bileşene çıkarmayı unuttuğunuz kodlarınızın olduğu anlamına gelir. Örneğin,posts
gibi veri prop’larını o veriyi direkt kullanmayan görsel bileşenlere aktarıyor olabilirsiniz, mesela<Layout posts={posts} />
. Bunun yerine,Layout
’un alt bileşeninichildren
olarak almasını sağlayın ve<Layout><Posts posts={posts} /></Layout>
olarak render edin. Bu kullanım, veriyi sağlayan ile veriye ihtiyaç duyan bileşenler arasındaki katman sayısını azaltır.
Eğer bu yaklaşımların ikiside işinize yaramıyor ise, o zaman context’i kullanmayı düşünebilirsiniz.
Context’in kullanım alanları
- Tema: Uygulamanız kullanıcının görünümü değiştirmesine izin veriyorsa (mesela karanlık mod), uygulamanızın en üstüne bir context provider koyabilir ve bu context’i görsel görünümlerini değiştirmesi gereken bileşenlerde kullanabilirsiniz.
- Çevrimiçi hesap: Bir çok bileşenin o anda oturum açmış olan kullanıcıyı bilmesi gerekebilir. Bunu bir context’e yerleştirmek, ağacın herhangi bir yerinde okumayı kolaylaştırır. Bazı uygulamalar aynı anda birden fazla hesabı çalıştırmanıza da izin verir (örneğin, farklı bir kullanıcı olarak yorum bırakmak için). Bu gibi durumlarda, kullanıcı arayüzünün bir kısmını farklı geçerli kullanıcıya sahip provider’a sarmalamak uygun olabilir.
- Routing: Çoğu routing çözümü, geçerli yolu tutmak için dahili olarak context kullanır. Linkler aktif olup olmadığını bu şekilde “bilir”. Kendi yönlendiricinizi oluşturuyorsanız, siz de bunu yapmak isteyebilirsiniz.
- State yönetimi: Uygulamanız büyüdükçe, uygulamanızın üst kısmına yakın çok sayıda state ile karşılaşabilirsiniz. Farklı derinlikteki birçok bileşen bunları değiştirmek isteyebilir. Karmaşık state’leri yönetmek ve çok fazla güçlük çekmeden uzaktaki bileşenlere aktarmak için context ile birlikte bir reducer kullanmak yaygındır.
Context kullanımı, statik değerlerle sınırlı değildir. Bir sonraki render’da farklı bir değer iletirseniz, React onu okuyan tüm bileşenleri günceller! Bu yüzden context genellikle state ile birlikte kullanılır.
Genellikle, bazı bilgilere ağacın farklı bölümlerindeki bileşenler tarafından ihtiyaç duyulması, context’in işinize yarayacağına dair güzel bir göstergedir.
Özet
- Context, bir elemanın altındaki tüm ağaca bilgi aktarmasını sağlar.
- Context’i aktarmak için:
export const MyContext = createContext(defaultValue)
ile oluşturun ve dışa aktarın.- Farklı derinlikteki herhangi bir alt bileşenden okumak için
useContext(MyContext)
Hook’una aktarın. - Üst bileşenden değer sağlamak için, alt bileşenleri
<MyContext.Provider value={...}>
içine sarın.
- Context ortada bulunan herhangi bir elamandan aktarılır.
- Context, “çevresine adapte olan” bileşenler yazmanıza olanak sağlar.
- Context kullanmadan önce, prop olarak aktarmayı veya JSX’i
children
olarak iletmeyi deneyin.
Problem 1 / 1: Prop drilling yerine context kullanmak
Bu örnekte, onay kutusunun (checkbox) işaretini değiştirmek, her <PlaceImage>
bileşenine aktarılan imageSize
prop’unu değiştirir. Onay kutusunun state’i en üst kapsam olan App
bileşeninde tutulur her <PlaceImage>
’ın bundan haberdar olması gerekir.
Şu anda, App
imageSize
değerini List
’e, List
de PlaceImage
’a aktarmaktadır. imageSize
prop’unu kaldırın ve bunun yerine App
bileşeninden doğrudan PlaceImage
’a aktarın.
Context tanımını Context.js
dosyasında yapabilirsiniz.
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); }} /> Büyük resimleri kullan </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} /> ); }