useSyncExternalStore
useSyncExternalStore
adalah sebuah Hook React yang membiarkan Anda berlangganan ke tempat penyimpanan eksternal.
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
Referensi
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
Panggil useSyncExternalStore
di tingkat paling atas dari komponen Anda untuk membaca sebuah nilai dari tempat penyimpanan data eksternal.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
Hook ini mengembalikan sebuah snapshot dari data yang ada di tempat penyimpanan. Anda harus memberikan dua fungsi sebagai argumen:
- Fungsi
subscribe
harus berlangganan ke tempat penyimpanan dan mengembalikan fungsi untuk berhenti berlangganan. - Fungsi
getSnapshot
harus membaca sebuah snapshot dari data yang ada di tempat penyimpanan.
Lihat lebih banyak contoh di bawah.
Parameter
-
subscribe
: Sebuah fungsi yang menerima sebuah argumencallback
dan berlangganan ke tempat penyimpanan. Saat tempat penyimpanan berubah, fungsi ini akan memanggilcallback
. Ini akan menyebakan komponen di-render ulang. Fungsisubscribe
harus mengembalikan fungsi yang membersihkan langganan tersebut. -
getSnapshot
: Sebuah fungsi yang mengembalikan sebuah snapshot dari data, di tempat penyimpanan, yang dibutuhkan komponen. Saat tempat penyimpanan masih belum berubah, pemanggilangetSnapshot
berulang kali tetap harus mengembalikan nilai yang sama. Jika tempat penyimpanan berubah dan nilai kembalian juga berubah (saat dibandingkan denganObject.is
), React me-render ulang komponen tersebut. -
opsional
getServerSnapshot
: Sebuah fungsi yang mengembalikan snapshot awal dari data yang ada di tempat penyimpanan. Snapshot hanya akan digunakan saat proses render dilakukan oleh server dan saat hidrasi konten yang telah di-render oleh server ke klien. Snapshot server harus sama antara klien dan server, dan biasanya diserialisasi dan diserahkan dari server ke klien. Jika Anda mengabaikan argumen ini, proses render komponen di server akan memunculkan kesalahan.
Kembalian
Snapshot saat ini dari tempat penyimpanan yang dapat Anda gunakan di logika render Anda.
Caveat
-
Snapshot tempat penyimpanan yang dikembalikan
getSnapshot
tidak boleh bisa dimutasi. Jika tempat penyimpanan mengandung data yang dapat dimutasi, Anda harus mengembalikan snapshot yang tidak dapat dimutasi saat data berubah. Jika data tidak berubah, Anda dapat mengembalikan snapshot terakhir yang sudah di-cache. -
Jika fungsi
subscribe
yang berbeda diberikan saat render ulang, React akan berlangganan ulang ke tempat penyimpanan menggunakan fungsisubscribe
yang baru. Anda bisa menghindari ini dengan mendeklarasisubscribe
di luar komponen. -
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 />;}
Penggunaan
Berlangganan ke tempat penyimpanan eksternal
Sebagian besar komponen React Anda akan membaca data dari props, state, dan context mereka. Walaupun begitu, kadang-kadang ada komponen yang harus membaca dari tempat penyimpanan yang ada di luar React dan berubah seiring waktu berjalan. Ini termasuk:
- Pustaka manajemen state dari pihak ketiga yang menyimpan state di luar React.
- API peramban yang mengekspos nilai yang dapat dimutasi dan event untuk berlangganan ke perubahannya.
Panggil useSyncExternalStore
di tingkat paling atas dari komponen Anda untuk membaca sebuah nilai dari tempat penyimpanan data eksternal.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
Fungsi ini mengembalikan snapshot dari data yang ada di tempat penyimpanan. Anda harus memberikan dua fungsi sebagai argumen:
- Fungsi
subscribe
harus berlangganan ke tempat penyimpanan dan mengembalikan sebuah fungsi untuk berhenti berlangganan. - Fungsi
getSnapshot
harus membaca sebuah snapshot dari data yang ada di tempat penyimpanan.
React akan menggunakan dua fungsi ini untuk menjaga status langganan komponen Anda ke tempat penyimpanan tersebut dan me-render ulang saat ada perubahan.
Misalnya, di sandbox di bawah, todosStore
diimplementasi sebagai tempat penyimpanan eksternal yang menyimpan data di luar React. Komponen TodosApp
terhubung ke tempat penyimpanan eksternal tersebut melalui Hook useSyncExternalStore
.
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()}>Add todo</button> <hr /> <ul> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); }
Berlangganan ke sebuah API peramban
Alasan lain untuk menggunakan useSyncExternalStore
adalah ketika Anda ingin berlangganan ke sebuah nilai yang diekspos peramban yang berubah seiring berjalannya waktu. Contohnya, saat Anda ingin komponen Anda menampilkan apakah koneksi jaringan masih aktif. Peramban mengekspos informasi ini melalui sebuah property yang disebut navigator.onLine
.
Nilai ini dapat berubah tanpa pengetahuan React sehingga Anda harus membacanya dengan useSyncExternalStore
.
import { useSyncExternalStore } from 'react';
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
Untuk mengimplementasi fungsi getSnapshot
, Anda cukup membaca nilai saat ini dari API peramban:
function getSnapshot() {
return navigator.onLine;
}
Selanjutnya, Anda perlu mengimplementasi fungsi subscribe
. Contohnya, saat navigator.onLine
berubah, peramban menembakkan event online
dan offline
ke objek window
. Anda perlu melanggankan argument callback
ke event yang bersangkutan, kemudian mengembalikan sebuah fungsi yang membersihkan langganan tersebut:
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
Sekarang React tahu bagaimana membaca nilai yang ada di API eksternal navigator.onLine
dan bagaimana berlangganan ke perubahannya. Putuskan koneksi perangkat Anda dari jaringan dan Anda akan melihat komponen di-render ulang sebagai respons:
import { useSyncExternalStore } from 'react'; export default function ChatIndicator() { const isOnline = useSyncExternalStore(subscribe, getSnapshot); return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</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); }; }
Mengekstrak logika ke Hook buatan sendiri
Biasanya Anda tidak akan menulis useSyncExternalStore
langsung di dalam komponen Anda. Alih-alih, Anda akan memanggil Hook
tersebut dari Hook
buatan Anda sendiri. Ini membiarkan Anda menggunakan tempat penyimpanan eksternal yang sama untuk berbagai komponen.
Sebagai contoh, Hook
useOnlineStatus
ini mengikuti apakah jaringan menyala:
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
// ...
}
function subscribe(callback) {
// ...
}
Sekarang, berbagai komponen bisa memanggil useOnlineStatus
tanpa harus mengulang implementasinya:
import { useOnlineStatus } from './useOnlineStatus.js'; function StatusBar() { const isOnline = useOnlineStatus(); return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>; } function SaveButton() { const isOnline = useOnlineStatus(); function handleSaveClick() { console.log('✅ Progress saved'); } return ( <button disabled={!isOnline} onClick={handleSaveClick}> {isOnline ? 'Save progress' : 'Reconnecting...'} </button> ); } export default function App() { return ( <> <SaveButton /> <StatusBar /> </> ); }
Menambahkan dukungan untuk render di server
Jika aplikasi React Anda melakukan render di server, komponen React Anda akan berjalan di luar lingkungan peramban untuk membuat HTML awal. Ini menimbulkan beberapa tantangan saat ingin berhubungan dengan tempat penyimpanan eksternal:
- Jika Anda berusaha untuk terhubung dengan API peramban, ini tidak akan bekerja karena API tersebut tidak ada di server.
- Jika Anda berusaha untuk terhubung dengan tempat penyimpanan pihak ketiga, Anda harus mencocokkan data yang ada di server dan klien.
Untuk menyelesaikan masalah ini, Anda dapat memberikan fungsi getServerSnapshot
sebagai argumen ketiga ke useSyncExternalStore
:
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isOnline;
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true; // Selalu menunjukkan "Online" untuk HTML yang dibuat server
}
function subscribe(callback) {
// ...
}
Fungsi getServerSnapshot
cukup mirip dengan getSnapshot
, tetapi hanya berjalan di dua situasi:
- Fungsi tersebut berjalan di server saat membuat HTML.
- Fungsi tersebut berjalan di klien saat hidrasi, misalnya saat React mengambil HTML dari server dan membuatnya interaktif.
Hal ini memungkinkan Anda untuk menyediakan nilai snapshot awal yang akan digunakan sebelum aplikasi menjadi interaktif. Jika tidak ada nilai awal yang cukup bermakna untuk proses render di server, Anda bisa mengabaikan argumen ini untuk memaksa proses render terjadi di klien.
Pemecahan masalah
Saya mendapat pesan kesalahan: “The result of getSnapshot
should be cached”
Pesan kesalahan ini berarti fungsi getSnapshot
mengembalikan sebuah objek baru pada setiap pemanggilannya, seperti:
function getSnapshot() {
// 🔴 Jangan selalu mengembalikan objek yang berbeda pada setiap pemanggilan
return {
todos: myStore.todos
};
}
React akan me-render ulang sebuah komponen jika getSnapshot
mengembalikan nilai yang berbeda dari sebelumnya. Ini mengapa, jika Anda selalu mengembalikan nilai yang berbeda, Anda akan melihat pengulangan tak berhingga dan mendapatkan pesan kesalahan ini.
Objek getSnapshot
Anda hanya mengembalikan objek yang berbeda jika memang ada yang berubah. Jika tempat penyimpanan Anda mengandung data yang tidak dapat dimutasi, Anda dapat langsung mengembalikan data tersebut:
function getSnapshot() {
// ✅ Anda dapat mengembalikan data yang tidak dapat dimutasi
return myStore.todos;
}
Jika tempat penyimpanan Anda dapat dimutasi, fungsi getSnapshot
Anda harus mengembalikan snapshot yang tidak dapat dimutasi dari data tersebut. Ini berarti fungsi tersebut harus membuat objek baru, tetapi tidak pada setiap pemanggilan. Justru, fungsi tersebut sebaiknya menyimpan snapshot terakhir dan mengembalikan snapshot tersebut jika data belum berubah. Bagaimana Anda menentukan apakah data tersebut sudah berubah atau tidak bergantung kepada tempat penyimpanan Anda.
Fungsi subscribe
saya tidak dipanggil setelah tiap tahap render
Fungsi subscribe
ini ditulis di dalam komponen sehingga fungsi tersebut selalu berbeda di setiap render:
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// 🚩 Selalu fungsi berbeda sehingga React akan berlangganan ulang setiap render
function subscribe() {
// ...
}
// ...
}
React akan berlangganan ulang ke tempat penyimpanan Anda jika Anda memberikan fungsi subscribe
berbeda antar-render. Jika ini memberikan masalah terhadap performa dan Anda ingin menghindari proses berlangganan ulang, Anda dapat memindahkan fungsi subscribe
keluar:
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
// ✅ Selalu fungsi yang sama sehingga React tidak perlu berlangganan ulang
function subscribe() {
// ...
}
Cara alternatif adalah dengan membungkus subscribe
ke dalam useCallback
untuk berlangganan ulang hanya jika beberapa argumen berubah:
function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ✅ Fungsi yang sama selama userId tidak berubah
const subscribe = useCallback(() => {
// ...
}, [userId]);
// ...
}