useSyncExternalStore membuat komponen berlangganan ke data yang berada di luar React dan merendernya lagi setiap kali data itu berubah. Kamu memberinya dua fungsi: subscribe, yang memberi tahu React cara mendengarkan perubahan, dan getSnapshot, yang mengembalikan nilai saat ini.
Kedua komponen Display tidak berbagi props maupun context, tapi keduanya diperbarui bersama, karena keduanya berlangganan ke store yang sama. Tombolnya memanggil fungsi biasa, bukan setter React. Tambahkan <Display name="Sidebar" /> ketiga dan komponen itu ikut bergabung tanpa perubahan lain.
Sintaksnya
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
subscribe(callback)mulai mendengarkan, memanggilcallbacksetiap kali datanya mungkin berubah, dan mengembalikan fungsi unsubscribe. React memanggilnya setelah komponen di-mount dan memanggil fungsi yang dikembalikan saat unmount.getSnapshot()mengembalikan nilai saat ini. React memanggilnya di setiap render dan setelah setiap notifikasi, lalu membandingkan hasilnya dengan yang terakhir memakaiObject.is. Nilainya sama, tidak ada render.getServerSnapshot()(opsional) mengembalikan nilai yang dipakai di server dan saat hidrasi.
Definisikan subscribe di luar komponen, atau jaga agar tetap stabil dengan useCallback. Kalau kamu memberikan fungsi subscribe baru di setiap render, React berhenti berlangganan lalu berlangganan lagi setiap kali.
API browser sebagai store
Apa pun yang punya nilai saat ini dan memicu event saat nilai itu berubah cocok dengan bentuk ini. navigator.onLine dengan event online dan offline adalah contoh klasiknya.
Matikan jaringanmu (atau pilih Offline di developer tools browser-mu) dan teksnya berubah tanpa memuat ulang. Argumen ketiga menyatakan "anggap online" saat merender di server, tempat tidak ada navigator. Membungkus pemanggilan hook itu di useOnlineStatus menjadikannya custom hook yang bisa dipakai komponen mana pun.
Lebar window bekerja dengan cara yang sama:
Tambahkan beberapa salinan lalu ubah ukuran window: setiap salinan menampilkan angka yang sama di saat yang sama. Setiap salinan punya listener-nya sendiri, dan masing-masing membaca lebarnya saat render, jadi tidak ada yang tertinggal satu frame pun.
getSnapshot harus mengembalikan nilai yang di-cache
React sering memanggil getSnapshot dan membandingkan hasilnya berdasarkan referensi. Fungsi yang membuat object atau array baru di setiap pemanggilan selalu terlihat seperti perubahan:
// Broken: a new object on every call
function getSnapshot() {
return { count: store.count, user: store.user };
}
// Also broken: filter returns a new array every time
function getSnapshot() {
return store.todos.filter((t) => !t.done);
}
React merender, memanggil getSnapshot, mendapat nilai yang "berbeda", merender lagi, dan seterusnya, sampai berhenti dengan "Maximum update depth exceeded". Di development, React juga lebih dulu mencatat "The result of getSnapshot should be cached to avoid an infinite loop". Build production, seperti pratinjau di sini, melewatkan peringatan itu dan hanya melaporkan error akhirnya sebagai kode singkat (Minified React error #185).
Perbaikannya adalah menjaga data di store tetap immutable: ganti object-nya saat berubah, dan kembalikan referensi yang tersimpan apa adanya.
getSnapshot mengembalikan object state yang sama sampai add menggantinya, jadi komponen dirender sekali per perubahan. Pemfilteran terjadi di komponen, setelah snapshot dibaca, dan itu aman. Untuk memfilter di dalam store, hitung array hasil filter saat datanya berubah lalu simpan, sehingga getSnapshot bisa mengembalikan salinan yang tersimpan itu.
getServerSnapshot dan hidrasi
Di server tidak ada window, tidak ada navigator, dan tidak ada subscription. Argumen ketiga memberi tahu React apa yang harus dirender di sana:
const width = useSyncExternalStore(
subscribe,
() => window.innerWidth, // in the browser
() => 1024 // on the server, and during hydration
);
React juga memakai getServerSnapshot untuk render pertama di browser saat menghidrasi HTML dari server, agar keduanya cocok. Tepat setelah hidrasi, React membaca getSnapshot dan, kalau nilai sebenarnya berbeda, merender lagi dengan nilai itu. Tanpa argumen ketiga, render di server melempar "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering." Kalau ada boundary <Suspense> di atas komponen, server mengirim fallback dari boundary itu dan browser merender kontennya; tanpa boundary, render di server gagal.
useSyncExternalStore vs useEffect dan useState
Kamu bisa berlangganan dengan sebuah effect:
function useOnlineStatus() {
const [online, setOnline] = useState(true);
useEffect(() => {
const update = () => setOnline(navigator.onLine);
update();
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
return online;
}
Cara ini berfungsi, dengan dua kelemahan. Render pertama selalu menampilkan tebakan awal, dan nilai sebenarnya datang satu render kemudian, setelah effect berjalan. Dan dengan concurrent rendering (saat transition, misalnya), React bisa menjeda render di tengah jalan; kalau store berubah selama jeda itu, komponen yang dirender sebelum dan sesudahnya bisa menampilkan nilai yang berbeda. Ketidakkonsistenan itu disebut tearing. useSyncExternalStore membaca nilainya saat render dan membuat React mengulang render secara sinkron kalau store berubah, sehingga setiap komponen melihat nilai yang sama.
Pakai hook ini saat datanya berada di luar React: module store milikmu sendiri, API browser, library pihak ketiga. Sebagian besar library state (Redux, Zustand, dan lainnya) memanggilnya untukmu di dalam hook mereka. Untuk data yang menjadi milik komponenmu, useState, useReducer, dan context tetap menjadi alat yang tepat.
Pertanyaan yang sering diajukan
Untuk apa useSyncExternalStore dipakai?
Untuk membaca data yang bukan milik React dan bisa berubah dengan sendirinya: store yang ditulis di luar React, library state pihak ketiga, atau nilai browser seperti navigator.onLine atau lebar window. Komponen dirender lagi setiap kali store memberi tahu React bahwa ada perubahan.
Apa fungsi subscribe dan getSnapshot?
subscribe(callback) mulai mendengarkan store, memanggil callback di setiap perubahan, dan mengembalikan fungsi untuk berhenti mendengarkan. getSnapshot() mengembalikan nilai saat ini. React memanggil getSnapshot saat render dan setelah setiap notifikasi, dan merender lagi hanya kalau nilainya berubah menurut Object.is.
Mengapa getSnapshot harus mengembalikan nilai yang di-cache?
React membandingkan hasil setiap pemanggilan getSnapshot dengan hasil sebelumnya. Kalau fungsi itu mengembalikan object atau array baru setiap kali, React selalu melihat perubahan, merender lagi, memanggil getSnapshot lagi, dan terus berulang sampai melempar "Maximum update depth exceeded". Kembalikan referensi yang sama sampai datanya benar-benar berubah.
Apa itu getServerSnapshot?
Argumen ketiga yang opsional. Fungsi ini mengembalikan nilai yang dipakai saat server rendering dan saat hidrasi di browser, sehingga keduanya menghasilkan HTML yang sama. Tanpanya, komponen melempar "Missing getServerSnapshot" di server, dan konten di bawah boundary <Suspense> terdekat dirender di browser sebagai gantinya.
Sebaiknya pakai useSyncExternalStore atau useEffect dengan useState?
Untuk berlangganan ke data dari luar, utamakan useSyncExternalStore. Hook ini membaca nilainya saat render, jadi render pertama sudah benar dan setiap komponen melihat nilai yang sama bahkan saat concurrent rendering. useEffect dengan useState merender sekali dengan nilai yang usang dan sesaat bisa menampilkan nilai berbeda di komponen yang berbeda.