React Context Bağlam Cehennemi Nedir?
React Context yuvalama cehennemi, aşağıdaki şekilde gösterildiği gibi React Context Provider’ın çok katmanlı yuvalanmasına atıfta bulunur . Ön uç problemindeki geri çağırma cehennemine benzer şekilde , giderek daha fazla Context daha büyük ve daha büyük yuvalama katmanlarına yol açacak ve bu da son derece zayıf kod okunabilirliğiyle sonuçlanacaktır.
Neden Context Nesting Hell gibi kodlar yazıyoruz?
Context, bileşenler arasında durum şeffaf iletimi gerçekleştirmemizi ve böylece durum paylaşımı amacına ulaşmamızı sağlayan React’ın bağlamsal durum yönetim API’sidir. Ancak Context’in bir performans sorunu var. Context birden fazla state niteliği içerdiğinde, state değiştirildiğinde, React’ın re-render özelliği nedeniyle, Context’e bağlı olan tüm bileşenler, Context’e bağlı olan bazı bileşenlerin state değerleri değişmemiş olsa bile, yeniden render edilecektir . Kod örneği şu şekildedir:
Bağlamı Tanımla (context.ts):
import { createContext } from 'react'
export const AppContext = createContext<{
theme: 'dark' | 'light',
count: number,
increase: () => void
}>({
theme: 'dark',
count: 0,
increase() {}
})
Bağlamın Sağlanması ve Tüketilmesi (page.tsx):
import { use, useState } from "react"
import { AppContext } from "./context"
export default function App() {
const [count, setCount] = useState(0)
const [theme, setTheme] = useState<'dark' | 'light'>('dark')
return (
<AppContext.Provider value={{ count, theme, increase: () => setCount(count + 1) }}>
<Header />
<Button />
</AppContext.Provider>
)
}
function Header() {
const { theme } = use(AppContext)
console.log('Header rendered')
return (
<header className={theme}>Header rendered, cur theme: {theme}</header>
)
}
function Button() {
const { count, increase } = use(AppContext)
console.log('Button rendered')
return (
<button onClick={increase}>you click me {count} times.</button>
)
}
Yukarıdaki kodda, AppContext’teki durumu değiştirmek için Button’a her tıkladığınızda { count: 0}
, Header count’u tüketmese bile, yeniden işlenecektir. Bunun nedeni, Header bileşeninin AppContext’i tüketmesi (abone olması) ve React’in context’e abone olan bileşenleri güncellemesidir. Durum değiştiğinde count
yeniden oluşturma tetiklenir ve alt bileşene geçirilen “bağlam” durumu da yeni bir nesnedir ve bağlam aboneliği güncellemesini tetikler.

Peki nasıl çözülür? Önemli olan, durumun değişip değişmediğidir ve güncellenmesi gerekmeyen bileşenlerin durumunun değişmeden kalmasını isteriz. Bunu yapmak için bağlamı ayrı Provider
bileşenlere bölmemiz gerekir. Her Sağlayıcıdaki durum değiştiğinde, React yukarıdan aşağıya güncellenecek ve yalnızca bu bağlamı tüketen bileşenleri yeniden işleyecektir; bu da durum izolasyonunu elde etmeye eşdeğerdir.
Örnek kod şu şekildedir:
(1) Bağlamı böl (context.ts):
import { createContext } from 'react'
// CounterContext
export const CounterContext = createContext<{
count: number,
increase: () => void
}>({
count: 0,
increase() {}
})
// ThemeContext
export type Theme = 'dark' | 'light'
export const ThemeContext = createContext<{
theme: Theme,
toggle: () => void
}>({ theme: 'dark', toggle() {} })
(2) Sağlayıcıyı böl ve durumu izole et:
import { use, useState } from "react"
import { CounterContext, Theme, ThemeContext } from "./context"
// ThemeContext
function ThemeContextProvider({ children } : { children ?: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('dark')
const toggle = () => setTheme(theme === 'dark' ? 'light' : 'dark')
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
)
}
// CounterContext
function CounterContextProvider({ children } : { children ?: React.ReactNode }) {
const [count, setCount] = useState(0)
const increase = () => setCount(count + 1)
return (
<CounterContext.Provider value={{ count, increase }}>
{children}
</CounterContext.Provider>
)
}
export default function App() {
return (
<ThemeContextProvider>
<CounterContextProvider>
<Header />
<Button />
</CounterContextProvider>
</ThemeContextProvider>
)
}
// ThemeContext
function Header() {
const { theme, toggle } = use(ThemeContext)
console.log('Header rendered')
return (
<header className={theme} onClick={toggle}>Header rendered, cur theme: {theme}</header>
)
}
// CounterContext
function Button() {
const { count, increase } = use(CounterContext)
console.log('Button rendered')
return (
<button onClick={increase}>you click me {count} times.</button>
)
}
Daha sonra bağlam sayısı arttıkça iç içe geçmiş düzey sayısı da artacak ve bu da Bağlam iç içe geçme cehennemi problemini oluşturacaktır.
export default function Page() {
return (
<ThemeContextProvider>
<CounterContextProvider>
<OtherContextProvider>
// ... Daha Fazla Yuvalama context
<App />
</OtherContextProvider>
</CounterContextProvider>
</ThemeContextProvider>
)
}
Not: Bağlam yeniden oluşturma sorununa yönelikburada listelenmeyen başka çözümler vb. de vardır. Bir yandan, bu makalenin odak noktası iç içe geçmiş cehennemi tanıtmaktır
memo
.useMemo
Öte yandan React’ın felsefelerinden birinin, kodun sürdürülebilirliğini ve okunabilirliğini artırmak için mümkün olduğunca optimizasyonla ilgili kancalardan kaçınmak olduğu iyi bilinmektedir.
Bir satır kod, bağlam yuvalama cehennemini zarif bir şekilde çözer
Yukarıdaki çok katmanlı iç içe geçmiş bileşenlere baktığımızda , aslında bunun, alt bileşenleri en üst düzey bileşenlere doldurarak ve daha sonra alt bileşenleri alt bileşenlere doldurarak sürekli olarak alt bileşen iç içe geçmiş bebekleri oluşturma süreci olduğunu görebiliriz. Alt bileşenleri bileşenlere manuel olarak yerleştirmenin bir yolu var mı? Cevap, React.cloneElement
mevcut bir React öğesini klonlamak için kullanılır ReactElement
ve ona yeni öğeler ekleyebilir props
veya mevcut öğeleri değiştirebilir props
ve alt bileşenler ekleyebilir.
İşte React.cloneElement
yöntem tanımı:
<code>React.cloneElement(element, [props], [...children])<br></code>
element
: Klonlanacak React öğesiprops
: İsteğe bağlı parametre, klonlanmış öğeye eklenecek yeni öznitelikleri veya üzerine yazılacak mevcut öznitelikleri içeren bir nesnechildren
: İsteğe bağlı parametre, yeni alt öğe (bileşen) orijinal alt öğenin yerini alacaktır
Daha sonra orijinal çok katmanlı iç içe kod düzleştirilebilir:
export default function Page() {
const comp1 = React.cloneElement(<OtherContextProvider />, {}, <><Header /><Button /></>)
const comp2 = React.cloneElement(<CounterContextProvider />, {}, comp1)
const comp3 = React.cloneElement(<ThemeContextProvider />, {}, comp2)
return comp3
}
reduceRight
Yukarıdaki kodun şu yöntemle optimize edilebileceği görülebilir :
export default function Page() {
return [
<ThemeContextProvider />,
<CounterContextProvider />,
<OtherContextProvider />,
<><Header /><Button /></>
].reduceRight((pre, cur) => React.cloneElement(pre, {}, cur))
}
MultiProviders fonksiyonunu özetlemeye devam ediyoruz ve sonunda konuya geliyoruz! Bu yöntemin özü sadece basit bir kod satırıdır ve çözüm budur 🎉🎉🎉!
/**
* :: Çok katmanlı Sağlayıcının kapsüllenmesi
*
* @param providers
* @returns 组件树
*/
type MultiProvidersPropsType = { providers: React.ReactElement[], children: React.ReactNode}
function MultiProviders({ providers, children } : MultiProvidersPropsType) {
return (<>
{ providers.reduceRight((pre, cur) => React.cloneElement(cur, {}, pre), children) }
</>)
}
export default function Page() {
return (
<MultiProviders providers={[
<ThemeContextProvider />,
<CounterContextProvider />,
<OtherContextProvider />
]}>
<App />
</MultiProviders>
)
}
function App() {
return (<>
<Header />
<Button />
</>)
}
Özellikleri:
- Kod yapısı açıktır ve iç içe geçmiş bir yapı yoktur
- Bağlamı bölün, durumu izole edin ve gereksiz işlemeyi önleyin
Özetle
- Genel fikir: birden fazla durumu tek bir Bağlam yönetiminde birleştirmek gereksiz güncellemelere yol açacaktır => Gereksiz güncellemeleri önlemek için Bağlamı ve Sağlayıcıyı Böl => Bir iç içe geçme cehennemi sorunu var => Bileşenlerin iç içe geçmesi aslında alt bileşenlerin sürekli oluşturulmasıdır =>
React.cloneElement
+reduceRight
Alt bileşenleri oluşturmak için döngü - Elbette, durum yönetimi daha karmaşıksa, en doğrudan çözüm Context kullanmamaktır. Belirli duruma bağlı olarak,
React.useReducer
Redux/Mobx/Valtio/Jotai/Zustand gibi durum yönetimi kütüphanelerini kullanabilirsiniz.
