useSyncExternalStore
useSyncExternalStore
, harici veri depolarına (store) abone olmanızı sağlayan React Hook’udur.
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
- Store nasıl çevrilecek?
Referans
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
Harici veri deposundan değer okumak için bileşeninizin en üst kapsamında useSyncExternalStore
’u çağırın.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
Depodaki verinin anlık görüntüsünü döndürür. Argüman olarak iki fonksiyon geçmeniz gerekir:
subscribe
fonksiyonu, depoya (data store) abone olmalı (subscribe) ve abonelikten çıkmak için fonksiyon döndürmelidir.getSnapshot
fonksiyonu, depodaki verinin anlık görüntüsünü okumalıdır.
Daha fazla örnek için aşağıya bakın.
Parametreler
-
subscribe
: Bircallback
argümanı alan ve depoya abone olan fonksiyondur. Depo değiştiğinde, iletilencallback
çalıştırılır. Bu, bileşenin yeniden render edilmesine neden olur.subscribe
fonksiyonu, aboneliği temizleyen bir fonksiyon döndürmelidir. -
getSnapshot
: Bileşenin ihtiyaç duyduğu depodaki verilerin anlık görüntüsünü döndüren fonksiyondur. Veri deposu değişmemişse,getSnapshot
’a yapılan çağrılar aynı değeri döndürmelidir. Depo değişirse ve döndürülen değer farklıysa (Object.is
ile karşılaştırıldığında), bileşen yeniden render edilir. -
isteğe bağlı
getServerSnapshot
: Depodaki verilerin başlangıçtaki anlık görüntüsünü döndüren fonksiyondur. Yalnızca sunucu taraflı render ya da istemcide render edilmiş çıktının hidratlanması sırasında çalıştırılır. Serileştirilerek sunucudan istemciye iletilen sunucu anlık görüntüsü, istemci ile aynı olmalıdır. Bu argümanı iletirseniz, bileşen sunucu tarafında render edilirken hata fırlatır.
Dönüş değeri
Render mantığınızda kullanabileceğiniz deponun o anki anlık görüntüsüdür.
Dikkat edilmesi gerekenler
-
getSnapshot
tarafından döndürülen depo anlık görüntüsü değiştirilemez (immutable) olmalıdır. Depoda değiştirilebilir veri varsa veriler değiştiğinde yeni bir anlık görüntü döndürün. Aksi takdirde, önbelleğe alınmış en son anlık görüntüyü döndürün. -
Yeniden render esnasında farklı bir
subscribe
fonksiyonu geçildiğinde React, yeni geçilensubscribe
fonksiyonu ile depoya yeniden abone olur.subscribe
’ı bileşenin dışında tanımlayarak bunu önleyebilirsiniz. -
If the store is mutated during a non-blocking transition update, React will fall back to performing that update as blocking. Specifically, for every transition update, React will call
getSnapshot
a second time just before applying changes to the DOM. If it returns a different value than when it was called originally, React will restart the update from scratch, this time applying it as a blocking update, to ensure that every component on screen is reflecting the same version of the store. -
It’s not recommended to suspend a render based on a store value returned by
useSyncExternalStore
. The reason is that mutations to the external store cannot be marked as non-blocking transition updates, so they will trigger the nearestSuspense
fallback, replacing already-rendered content on screen with a loading spinner, which typically makes a poor UX.For example, the following are discouraged:
const LazyProductDetailPage = lazy(() => import('./ProductDetailPage.js'));function ShoppingApp() {const selectedProductId = useSyncExternalStore(...);// ❌ Calling `use` with a Promise dependent on `selectedProductId`const data = use(fetchItem(selectedProductId))// ❌ Conditionally rendering a lazy component based on `selectedProductId`return selectedProductId != null ? <LazyProductDetailPage /> : <FeaturedProducts />;}
Kullanım
Harici depoya abone olma
React bileşenlerinizin çoğu veriyi yalnızca prop, state ve context’den okur. Ancak bileşenler, bazı verileri React dışındaki bir depodan (store) okuma ihtiyacı duyabilir. Aşağıdaki durumlar buna örnektir:
- React dışında state tutan üçüncü parti state yönetim kütüphaneleri.
- Değiştirebilir değer ve değişikliklere abone olmak için olaylar (event) sunan tarayıcı API’leri.
Harici veri deposundan bir değer okumak için bileşeninizin en üst kapsamında useSyncExternalStore
’u çağırın.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
Veri deposundaki verilerin anlık görüntüsünü döndürür. Argüman olarak iki fonksiyon geçmeniz gerekir:
subscribe
fonksiyonu, depoya abone olmalı ve aboneliği sonlandıran fonksiyon döndürmelidir.getSnapshot
fonksiyonu, depodan veriyi anlık görüntüsünü okumalıdır.
React, bu fonksiyonları kullanarak bileşeninizi depoya abone tutar ve değişikliklerde yeniden render eder.
Aşağıdaki örnekte todosStore
, React’ın dışında veri tutan harici bir depo olacak şekilde implemente edilmiştir. TodosApp
bileşeni useSyncExternalStore
Hook’u ile harici depo ile bağlantı kurar.
import { useSyncExternalStore } from 'react'; import { todosStore } from './todoStore.js'; export default function TodosApp() { const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot); return ( <> <button onClick={() => todosStore.addTodo()}>Yapılacak iş ekle</button> <hr /> <ul> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); }
Tarayıcı API’sine abone olma
useSyncExternalStore
kullanmak için başka bir neden, tarayıcı tarafından sunulan ve zamanla değişen değerlere abone olmaktır. Örneğin, bileşeninizde ağ bağlantısının etkin olup olmadığını göstermek istiyorsunuzdur. Tarayıcı, bu bilgiyi navigator.onLine
özelliği aracılığıyla sunar.
Bu değer React’ın bilgisi dışında değişebilir ve bu sebeple useSyncExternalStore
ile okumanız gerekir.
import { useSyncExternalStore } from 'react';
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
getSnapshot
fonksiyonunu implemente etmek için tarayıcı API’sinden geçerli değeri okuyun:
function getSnapshot() {
return navigator.onLine;
}
Ardından, subscribe
fonksiyonunu implemente etmeniz gerekir. Örneğin, navigator.onLine
değiştiğinde window
nesnesi üzerinden online
ve offline
olayları tetiklenir. callback
argümanıyla bu olaylara abone olmanız ve abonelikleri temizleyen bir fonksiyon döndürmeniz gerekir.
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
Artık React, harici navigator.onLine
API’sinin değerini nasıl okuyacağını ve değişikliklere nasıl abone olacağını bilir. Cihazınızın ağ bağlantısı kesin ve bileşenin buna karşılık yeniden render’ı tetiklediğine dikkat edin:
import { useSyncExternalStore } from 'react'; export default function ChatIndicator() { const isOnline = useSyncExternalStore(subscribe, getSnapshot); return <h1>{isOnline ? '✅ Çevrimiçi' : '❌ Bağlantı kesildi'}</h1>; } function getSnapshot() { return navigator.onLine; } function subscribe(callback) { window.addEventListener('online', callback); window.addEventListener('offline', callback); return () => { window.removeEventListener('online', callback); window.removeEventListener('offline', callback); }; }
Mantığı özel bir hook’a çıkarma
Usually you won’t write useSyncExternalStore
directly in your components. Instead, you’ll typically call it from your own custom Hook. This lets you use the same external store from different components.
For example, this custom useOnlineStatus
Hook tracks whether the network is online:
Genellikle useSyncExternalStore
’u bileşenlerinizde doğrudan kullanmazsınız. Bunun yerine kendi özel Hook’unuzda çağırırsınız.
Böylece aynı harici depoyu farklı bileşenlerden de kullanabilirsiniz.
Örneğin, örnekteki özel useOnlineStatus
Hook’u ağın çevrimiçi olup olmadığını takip eder:
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
// ...
}
function subscribe(callback) {
// ...
}
Artık farklı bileşenler, implementasyonu sürekli tekrarlamadan useOnlineStatus
çağırabilir:
import { useOnlineStatus } from './useOnlineStatus.js'; function StatusBar() { const isOnline = useOnlineStatus(); return <h1>{isOnline ? '✅ Çevrimiçi' : '❌ Bağlantı kesildi'}</h1>; } function SaveButton() { const isOnline = useOnlineStatus(); function handleSaveClick() { console.log('✅ İlerleme kaydedildi'); } return ( <button disabled={!isOnline} onClick={handleSaveClick}> {isOnline ? 'İlerlemeyi kaydet' : 'Yeniden bağlanılıyor...'} </button> ); } export default function App() { return ( <> <SaveButton /> <StatusBar /> </> ); }
Sunucu taraflı render desteği ekleme
React uygulamanız sunucu taraflı render’lama kullanıyorsa, React bileşenleriniz başlangıç HTML’ini üretmek için tarayıcı ortamının dışında da çalışacaktır. Bu durum, harici depoya bağlanırken bazı zorlukları beraberinde getirir:
- Yalnızca tarayıcıda bulunan bir API’ye bağlanıyorsanız, çalışmayacaktır çünkü sunucuda mevcut değildir.
- Üçüncü taraf bir veri deposuna bağlanıyorsanız, sunucu ve istemci arasında verilerin eşleşmesi gerekmektedir.
Bu sorunları çözmek için, useSyncExternalStore
’a üçüncü argüman olarak getServerSnapshot
fonksiyonunu iletin:
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isOnline;
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true; // Sunucu tarafında oluşturulan HTML için her zaman "Online" gösterir
}
function subscribe(callback) {
// ...
}
getServerSnapshot
fonksiyonu getSnapshot
’a benzer ancak yalnızca iki durumda çalışır:
- HTML oluşturulurken sunucuda çalışır.
- React’ın sunucu HTML’ini alıp etkileşimli haline getirirken yani hidratlama yaparken istemcide çalışır.
Bu durum, uygulama etkileşimli hale gelmeden önce kullanılacak olan başlangıç anlık görüntü değeri vermenizi sağlar. Sunucu taraflı render için anlamlı bir başlangıç değeriniz yoksa, istemcide render işlemini zorlamak için bu argümanı atlayın.
Sorun giderme
“The result of getSnapshot
should be cached” hatası alıyorum
Bu hata, getSnapshot
fonksiyonunuzun her çağırıldığında yeni bir nesne döndürdüğü anlamına gelir, örneğin:
function getSnapshot() {
// 🔴 getSnapshot'dan her seferinde farklı nesne döndürmeyin
return {
todos: myStore.todos
};
}
getSnapshot
son seferkinden farklı bir değer döndürdüğünde, React bileşeni yeniden render eder. Dolayısıyla her seferinde farklı sonuç döndürdüğünüzde sonsuz döngüye girer ve hata alırsınız.
getSnapshot
nesneniz yalnızca gerçekten değiştiğinde farklı bir nesne döndürür. Deponuz değişmez (immutable) veri içeriyorsa, bu verileri doğrudan döndürebilirsiniz:
function getSnapshot() {
// ✅ Değişmez verileri döndürebilirsiniz
return myStore.todos;
}
Deponuzdaki veri değişken (mutable) ise getSnapshot
fonksiyonunuz değişmez anlık görüntüsünü döndürmelidir. Yani her çağrıldığında farklı nesne oluşturması gerektiği anlamına gelir. Bunun yerine, son hesaplanan anlık görüntüyü depolamalı ve depodaki veri değişmediyse bir önceki anlık görüntüyü döndürmelidir. Değişken verilerin değişip değişmediğini nasıl belirleyeceğiniz deponuza bağlıdır.
subscribe
fonksiyonum her render’dan sonra çağırılıyor
Örnekteki subscribe
fonksiyonu bileşenin içinde tanımlanmıştır ve bu nedenle her render’da farklıdır:
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// 🚩 Her zaman farklı fonksiyondur, React her render'da yeniden abone olur
function subscribe() {
// ...
}
// ...
}
Yeniden render’lar arasında farklı bir subscribe
fonksiyonu iletirseniz, React deponuza yeniden abone olur. Bu durum performans sorunlarına neden oluyorsa ve sürekli abone olmaktan kaçınmak istiyorsanız, subscribe
fonksiyonunu bileşen dışına taşıyın:
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
// ✅ Her zaman aynı fonksiyondur, React yeniden abone olmaz
function subscribe() {
// ...
}
Alternatif olarak, yalnızca bir takım argümanlar değiştiğinde yeniden abone olmak için subscribe
fonksiyonunu useCallback
Hook’una sarın:
function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ✅ userId değişmediği sürece aynı fonksiyondur
const subscribe = useCallback(() => {
// ...
}, [userId]);
// ...
}