Tema tercihi gibi küçük bir durum bile yanlış yere konunca şaşırtıcı davranabilir: ilk açılışta renk yanıp söner, yenilemede seçim kaybolur veya iki bileşen farklı tema gösterir. Eski React/Next.js portfolio denememdeki istemci durumunu açıklarken bellekteki kaynak durum ile tarayıcıdaki kalıcı tercihi ayırıyorum.
İlk render tarayıcıya bağımlı olmak zorunda değil
Örnekte başlangıç dark. localStorage yalnız mount sonrasındaki effect içinde okunuyor. Böylece sunucuda window bulunmadığı için hata üretmiyoruz; sunucu ve ilk istemci render'ı aynı varsayımdan başlayabiliyor. React istemci bileşeni bağlamında kullanılmalı; bu kod tek başına bir sunucu oturumu oluşturmaz.
import {useEffect,useState} from 'react';
export function Preference(){
const [theme,setTheme]=useState('dark');
useEffect(()=>{
try{const saved=localStorage.getItem('theme');
if(saved==='light'||saved==='dark')setTheme(saved);
}catch{/* Browser storage may be unavailable. */}
},[]);
function toggle(){
const next=theme==='dark'?'light':'dark';
setTheme(next);
try{localStorage.setItem('theme',next);}catch{}
}
return <button onClick={toggle}>{theme}</button>;
}
Depodan gelen değer light veya dark ise kabul ediliyor; başka metinler yok sayılıyor. try/catch, tarayıcı depolaması kapalı veya erişilemez olduğunda düğmenin yine çalışmasına izin veriyor. State'i önce güncelleyip depoya yazmayı denediğimiz için görsel tercih, depolama başarısızlığına rehin kalmıyor.
Birden çok bileşen için Context
Yukarıdaki Preference kendi durumunu tutar. Aynı tema üst menü ve içerik alanında paylaşılacaksa her bileşende ayrı useState oluşturmak yerine tek bir Provider gerekir. Aşağıdaki bağımsız örnek, Context başlığının gerçek paylaşılan durum kısmını gösterir. Next.js içinde dosya bir client boundary altında olmalıdır.
import {createContext, useContext, useEffect, useState} from 'react';
import type {ReactNode} from 'react';
type Theme = 'dark' | 'light';
const ThemeContext = createContext<{
theme: Theme;
toggle: () => void;
} | null>(null);
export function ThemeProvider({children}: {children: ReactNode}) {
const [theme, setTheme] = useState<Theme>('dark');
const [loaded, setLoaded] = useState(false);
useEffect(() => {
try {
const saved = localStorage.getItem('portfolio.theme');
if (saved === 'light' || saved === 'dark') setTheme(saved);
} catch {}
setLoaded(true);
}, []);
useEffect(() => {
if (!loaded) return;
try { localStorage.setItem('portfolio.theme', theme); } catch {}
}, [theme, loaded]);
const toggle = () => setTheme(t => t === 'dark' ? 'light' : 'dark');
return <ThemeContext.Provider value={{theme, toggle}}>
{children}
</ThemeContext.Provider>;
}
export function useTheme() {
const value = useContext(ThemeContext);
if (!value) throw new Error('ThemeProvider is required');
return value;
}
loaded bayrağı ilk kaydetme effect'inin, depodaki tercih okunmadan varsayılan dark değerini yazmasını önler. State updater yalnız yeni değeri hesaplar; depoya yazma yan etkisi updater'ın içine konmaz. Provider altındaki tüketici useTheme ile aynı kaynağı okur. Context, localStorage içeriğini reaktif veriye kendiliğinden dönüştürmez; güncel ekranın kaynağı React state'idir.
Effect'in zamanı görünüm kararını etkiler
Tercih mount sonrası yüklendiğinden kayıtlı light temaya geçiş görülebilir. Sunucunun bir cookie üzerinden aynı tercihi bilmesi veya kontrollü erken tema uygulaması başka tasarım seçenekleridir. Hydration uyarısını gizlemek, iki ilk render arasındaki farkı çözmüş olmak değildir. Küçük uygulamada bu geçiş kabul edilebilir olabilir; karar görünüm ihtiyacına göre verilir.
React Strict Mode geliştirmede effect yaşam döngüsünü tekrar sınayabilir. Depo okuma/yazmanın aynı girdide aynı sonuca gitmesi sorun yaratmaz; fakat abonelik ekliyorsam cleanup gerekir. Başka sekmede değişen tercihi dinlemek için storage olayı kullanılabilir; aynı sekmenin kendi yazması için Context state güncellemesi zaten ayrı kanaldır.
Tercih ile yetkiyi aynı depoya emanet etmemek
Tarayıcı deposu kullanıcı tarafından değiştirilebilir. Tema için bu beklenen bir davranış; admin izni için güvenilir kanıt değil. isAdmin değerini true yapmak sunucuda yetki açmamalı. Anahtar öneki kullanmak da güvenlik sağlamaz ama aynı origin'deki uygulamaların birbirinin tercihini ezmesini azaltır.
Testte kayıtlı light, bilinmeyen değer, depolama exception'ı ve Provider dışı tüketim bulunmalı. Context örneği tüm uygulamanın durum yönetimi yerine geçmek için değil, gerçekten paylaşılan bir tercihin tek sahibini oluşturmak için var.
React useContext; Effect yaşam döngüsü ve sunucu render sınırı.