'Effect İçinde setState' Lint Hatasını useSyncExternalStore ile Çözmek
Bu sitede bir oyunlaştırma sistemi kurarken (localStorage'da tutulan bir puan durumu) aynı ESLint hatasına üç ayrı yerde çarptım:
Error: Calling setState synchronously within an effect can trigger
cascading renders
react-hooks/set-state-in-effect
Hatanın tipik şekli
En basit hali, mount olduktan sonra bir "hazır" bayrağı set eden klasik hydration-safe pattern:
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []); // ❌ lint hatası
Bu pattern, sunucu ile ilk client render'ın aynı çıktıyı üretmesini
sağlamak için çok yaygın kullanılır (localStorage gibi yalnızca
tarayıcıda var olan bir şeyi okumadan önce). Ama React'in yeni lint
kuralı, effect içinde doğrudan bir state setter çağırmayı — hatta bu
çağrı başka bir fonksiyonun içinden yapılsa bile — sorunlu buluyor.
Çözüm: useSyncExternalStore
React'in kendi önerdiği alternatif, useSyncExternalStore — üç
argüman alıyor: subscribe, getSnapshot (client için), ve
getServerSnapshot (SSR için). React, hydration sırasında otomatik
olarak getServerSnapshot'ı kullanıyor, hydration bittikten hemen
sonra gerçek getSnapshot'a geçiyor. Sonuç: hydration mismatch yok,
effect+setState yok.
function subscribe() {
return () => {};
}
function useMounted() {
return useSyncExternalStore(
subscribe,
() => true, // client: hep true
() => false, // server: hep false
);
}
Daha büyük ölçekte: modül seviyesinde bir store
Aynı prensip, localStorage'a yazan/okuyan daha karmaşık bir state
için de işe yarıyor — sadece React state yerine modül seviyesinde
mutable bir obje + dinleyici listesi kullanıyorsun:
let snapshot = { ids: [] as string[] };
let listeners: (() => void)[] = [];
export function subscribe(listener: () => void) {
listeners.push(listener);
return () => {
listeners = listeners.filter((l) => l !== listener);
};
}
export function getSnapshot() {
return snapshot;
}
export function getServerSnapshot() {
return { ids: [] };
}
export function unlock(id: string) {
if (snapshot.ids.includes(id)) return;
snapshot = { ids: [...snapshot.ids, id] };
listeners.forEach((l) => l());
}
unlock artık bir React state setter değil, düz bir fonksiyon —
istediğin yerden (bir effect'in içinden bile) güvenle çağırabilirsin,
çünkü lint kuralının aradığı "setState çağrısı" deseni ortada yok.
Bonus: key ile "taze mount" tekniği
Bir input'un her açılışta sıfırlanması gerektiğinde (örneğin bir komut
paleti), useEffect içinde setQuery("") çağırmak yerine, ilgili
alt bileşeni değişen bir key ile sarmak işe yarıyor — React, key
değiştiğinde bileşeni komple yeniden mount ediyor, useState'in
başlangıç değeri doğal olarak sıfırlanıyor, hiçbir effect gerekmiyor.