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:

  1. subscribe fonksiyonu, depoya (data store) abone olmalı (subscribe) ve abonelikten çıkmak için fonksiyon döndürmelidir.
  2. 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: Bir callback argümanı alan ve depoya abone olan fonksiyondur. Depo değiştiğinde, iletilen callback ç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çilen subscribe 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 nearest Suspense 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:

  1. subscribe fonksiyonu, depoya abone olmalı ve aboneliği sonlandıran fonksiyon döndürmelidir.
  2. 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>
    </>
  );
}

Not

Mümkün mertebe useState ve useReducer aracılığıyla yerleşik React state’ini kullanmanızı öneririz. useSyncExternalStore API’si, bileşenlerinizi React olmayan kodlarınızla entegre etmeniz gerektiğinde kullanışlıdır.


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.

Not

getServerSnapshot’ın istemci tarafındaki ilk render’da sahip olduğu verilerin, sunucudan döndürdüğü verilerle birebir aynı olduğundan emin olun. Örneğin getServerSnapshot sunucuda doldurulmuş olarak gelen depo içeriği döndürdüyse, bu içeriği istemciye aktarmanız gerekir. Bunun yapmanın bir yolu, sunucu taraflı render esnasında window.MY_STORE_DATA gibi bir global tanımlayan <script> etiketi kullanmak ve ardından istemcide getServerSnapshot içinden bu global değişkeni okumaktır. Harici deponuz bunu nasıl yapacağınıza ilişkin talimatlar sağlamalıdır.


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]);

// ...
}