useSyncExternalStore
useSyncExternalStore γ―γε€ι¨γΉγγ’γΈοΏ½?γ΅γγΉγ―γ©γ€γγε―θ½γ«γγ React οΏ½?γγγ―γ§γγ
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)γͺγγ‘γ¬γ³γΉ
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
ε€ι¨γγΌγΏγΉγγ’γγε€γθͺγΏεγγγγ«γγ³γ³γγΌγγ³γοΏ½?γγγγ¬γγ«γ§ useSyncExternalStore γεΌγ³εΊγγΎγγ
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}γγγ―γγΉγγ’γ«γγγγΌγΏοΏ½?γΉγγγγ·γ§γγγθΏγγΎγγεΌζ°γ¨γγ¦ 2 γ€οΏ½?ι’ζ°γζΈ‘γεΏ θ¦γγγγΎγοΌ
subscribeι’ζ°γ―γΉγγ’γΈοΏ½?γ΅γγΉγ―γ©γ€γγιε§γγΎγγγ΅γγΉγ―γ©γ€γγθ§£ι€γγι’ζ°γθΏγεΏ θ¦γγγγΎγγgetSnapshotι’ζ°γ―γγΉγγ’γγγγΌγΏοΏ½?γΉγγγγ·γ§γγγθͺγΏεγεΏ θ¦γγγγΎγγ
εΌζ°
-
subscribe: γΉγγ’γ«γ΅γγΉγ―γ©γ€γγιε§γγγΎγ callback εΌζ°γεγεγι’ζ°γγΉγγ’γε€ζ΄γγγιγ«ζΈ‘γγγ callback γεΌγ³εΊγεΏ θ¦γγγγΎγγγγγ«γγγγ³γ³γγΌγγ³γγεγ¬γ³γγΌγγγΎγγsubscribeι’ζ°γ―γγ΅γγΉγ―γͺγγ·γ§γ³γγ―γͺγΌγ³γ’γγγγι’ζ°γθΏγεΏ θ¦γγγγΎγγ -
getSnapshot: γ³γ³γγΌγγ³γγεΏ θ¦γ¨γγγΉγγ’γ«γγγγΌγΏοΏ½?γΉγγγγ·γ§γγγθΏγι’ζ°γγΉγγ’γε€ζ΄γγγ¦γγͺγε ΄εγgetSnapshotγΈοΏ½?εεΌγ³εΊγγ―εγε€γθΏγεΏ θ¦γγγγΎγγγΉγγ’γε€ζ΄γγγ¦θΏγγγε€γοΌObject.isγ§ζ―θΌγγ¦οΌη°γͺγε ΄εγReact γ―γ³γ³γγΌγγ³γγεγ¬γ³γγΌγγΎγγ -
ηη₯ε―θ½
getServerSnapshot: γΉγγ’οΏ½?γγΌγΏοΏ½?εζγΉγγγγ·γ§γγγθΏγι’ζ°γγγγ―γ΅γΌγγ¬γ³γγͺγ³γ°δΈγγγγ³γ―γ©γ€γ’γ³γδΈγ§οΏ½?γ΅γΌγγ¬γ³γγͺγ³γ°γγγγ³γ³γγ³γοΏ½?γγ€γγ¬γΌγ·γ§γ³δΈγ«οΏ½?γΏδ½Ώη¨γγγΎγγγ΅γΌγγΉγγγγ·γ§γγγ―γ―γ©γ€γ’γ³γγ¨γ΅γΌγιγ§εδΈγ§γͺγγγ°γͺγγγιεΈΈγ―γ΅γΌγγγγ―γ©γ€γ’γ³γγ«ζΈ‘γγγγ·γͺγ’γ©γ€γΊγγγγοΏ½?γ§γγγοΏ½?εΌζ°γηη₯γγγ¨γγ΅γΌγδΈγ§οΏ½?γ³γ³γγΌγγ³γοΏ½?γ¬γ³γγͺγ³γ°γ―γ¨γ©γΌγηΊηγγγΎγγ
θΏγε€
γ¬γ³γγͺγ³γ°γγΈγγ―γ§δ½Ώη¨γ§γγγΉγγ’οΏ½?ηΎε¨οΏ½?γΉγγγγ·γ§γγγ
注ζηΉ
-
getSnapshotγ«γγ£γ¦θΏγγγγΉγγ’οΏ½?γΉγγγγ·γ§γγγ―γ€γγ₯γΌγΏγγ«οΌimmutable; ζΈγζγδΈθ½οΌγ§γͺγγγ°γͺγγΎγγγθεΎγ§δ½Ώγ£γ¦γγγΉγγ’γγγ₯γΌγΏγγ«γͺγγΌγΏγζγ£γ¦γγε ΄εγγγΌγΏγε€ζ΄γγγε ΄εγ―ζ°γγγ€γγ₯γΌγΏγγ«γͺγΉγγγγ·γ§γγγθΏγγγγδ»₯ε€οΏ½?ε ΄εγ―γγ£γγ·γ₯γγγζεΎοΏ½?γΉγγγγ·γ§γγγθΏγγγγ«γγΎγγ -
εγ¬γ³γγΌδΈγ«η°γͺγ
subscribeι’ζ°γζΈ‘γγγε ΄εγReact γ―ζ°γγζΈ‘γγγsubscribeι’ζ°γδ½Ώγ£γ¦γΉγγ’γ«εγ΅γγΉγ―γ©γ€γγγΎγγγγγι²γγ«γ―γsubscribeγγ³γ³γγΌγγ³γοΏ½?ε€γ§οΏ½?οΏ½θ¨γγΎγγ -
γγ³γγγγγ³γ°εοΏ½?γγ©γ³γΈγ·γ§γ³ζ΄ζ°οΏ½?ζδΈγ«γΉγγ’οΏ½?ζΈγζγγηΊηγγε ΄εγReact γ―γοΏ½?ζ΄ζ°γγγγγγ³γ°εγ§θ‘γγγγ«γγ©γΌγ«γγγ―γγΎγγε ·δ½ηγ«γ―γReact γ― DOM γ«ζ΄ζ°γι©η¨γγεγ«
getSnapshotγεεΊ¦εΌγ³εΊγγΎγγγγγ§ζεοΏ½?ε€γ¨γ―η°γͺγε€γθΏγγγε ΄εγReact γ―γγ©γ³γΈγ·γ§γ³οΏ½?ζ΄ζ°γζεγγγγη΄γγΎγγγε試θ‘ζγ«γ―γγγγγ³γ°εοΏ½?ζ΄ζ°γθ‘γγγ¨γ§γη»ι’δΈοΏ½?ε ¨γ³γ³γγΌγγ³γγγΉγγ’γγοΏ½?εδΈγγΌγΈγ§γ³οΏ½?ε€γεζ γγ¦γγγγ¨γδΏθ¨ΌγγΎγγ -
useSyncExternalStoreγγθΏγγγε€γ«εΊγ₯γγ¦γ¬γ³γγΌγγ΅γΉγγ³γγγγγγ¨γ―ζ¨ε₯¨γγγ¦γγΎγγγε€ι¨γΉγγ’γ§θ΅·γγε€ζ΄γ―γγ³γγγγγ³γ°εοΏ½?γγ©γ³γΈγ·γ§γ³ζ΄ζ°γ¨γγ¦γγΌγ―γγγγ¨γγ§γγͺγγγγη΄θΏοΏ½?Suspenseγγ©γΌγ«γγγ―γθ΅·εγγ¦γγΎγγΎγγζ’γ«η»ι’δΈγ«θ‘¨η€Ίγγγ¦γγγ³γ³γγ³γγγγΌγγ£γ³γ°γΉγγγ§ι γγ¦γγΎγγγγιεΈΈγ―ζγΎγγγͺγγ¦γΌγΆδ½ι¨γ«γ€γͺγγγΎγγδΎγγ°δ»₯δΈοΏ½?γγγͺγ³γΌγγ―ζ¨ε₯¨γγγΎγγγ
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 />;}
δ½Ώη¨ζ³
ε€ι¨γΉγγ’γΈοΏ½?γ΅γγΉγ―γ©γ€γ
React γ³γ³γγΌγγ³γοΏ½?γ»γ¨γγ©γ―γpropsγstate γγγ³γ³γ³γγ―γΉγγγοΏ½?γΏγγΌγΏγθͺγΏεγγΎγγγγγγγ³γ³γγΌγγ³γγ―ζιγ¨ε ±γ«ε€εγγ React ε€οΏ½?γΉγγ’γγγγΌγΏγθͺγΏεγεΏ θ¦γγγε ΄εγγγγΎγγγγγ«γ―δ»₯δΈοΏ½?γγγͺγοΏ½?γε«γΎγγΎγοΌ
- React οΏ½?ε€ι¨γ§ηΆζ γδΏζγγγ΅γΌγγγΌγγ£οΏ½?ηΆζ οΏ½?οΏ½ηγ©γ€γγ©γͺγ
- ε―ε€οΏ½?ε€γγγοΏ½?ε€ζ΄γ«γ΅γγΉγ―γ©γ€γγγγγοΏ½?γ€γγ³γγ¨γγ«ε ¬ιγγγγ©γ¦γΆ APIγ
ε€ι¨γγΌγΏγΉγγ’γγε€γθͺγΏεγγγγ«γγ³γ³γγΌγγ³γοΏ½?ζδΈδ½γ§ useSyncExternalStore γεΌγ³εΊγγΎγγ
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}γγγ―γΉγγ’ε οΏ½?γγΌγΏοΏ½?γΉγγγγ·γ§γγγθΏγγΎγγεΌζ°γ¨γγ¦ 2 γ€οΏ½?ι’ζ°γζΈ‘γεΏ θ¦γγγγΎγοΌ
subscribeι’ζ°γ―γγΉγγ’γΈοΏ½?γ΅γγΉγ―γ©γ€γγθ‘γγγΎγγ΅γγΉγ―γ©γ€γγθ§£ι€γγι’ζ°γθΏγγΎγγgetSnapshotι’ζ°γ―γγΉγγ’γγγγΌγΏοΏ½?γΉγγγγ·γ§γγγθͺγΏεγγΎγγ
React γ―γγγοΏ½?ι’ζ°γδ½Ώγ£γ¦γ³γ³γγΌγγ³γγγΉγγ’γ«γ΅γγΉγ―γ©γ€γγγγηΆζ γ«δΏγ‘γε€ζ΄γγγγγ³γ«εγ¬γ³γγΌγγΎγγ
δΎγγ°γδ»₯δΈοΏ½?γ΅γ³γγγγ―γΉγ§γ―γtodosStore γ― React οΏ½?ε€ι¨γ«γγΌγΏγδΏεγγε€ι¨γΉγγ’γ¨γγ¦οΏ½?οΏ½θ£
γγγ¦γγΎγγTodosApp γ³γ³γγΌγγ³γγ―γ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> </> ); }
γγ©γ¦γΆ API γΈοΏ½?γ΅γγΉγ―γ©γ€γ
useSyncExternalStore γθΏ½ε γγγγ 1 γ€οΏ½?ηη±γ―γζιγ¨γ¨γγ«ε€εγγγγγ©γ¦γΆγε
¬ιγγε€γ«γ΅γγΉγ―γ©γ€γγγγε ΄εγ§γγγγ¨γγ°γγ³γ³γγΌγγ³γγγγγγ―γΌγ―ζ₯ηΆγγ’γ―γγ£γγγ©γγγ葨瀺γγγγ¨γγΎγγγγ©γ¦γΆγ―γγοΏ½?ζ
ε ±γ navigator.onLine γ¨γγγγγγγ£γδ»γγ¦ε
¬ιγγΎγγ
γοΏ½?ε€γ― React οΏ½?η₯γγͺγγ¨γγγ§ε€ζ΄γγγε―θ½ζ§γγγοΏ½?γ§γuseSyncExternalStore γ§γγγθͺγΏεγγΉγγ§γγ
import { useSyncExternalStore } from 'react';
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}getSnapshot ι’ζ°γοΏ½?οΏ½θ£
γγγγγ«γ―γγγ©γ¦γΆ API γγηΎε¨οΏ½?ε€γθͺγΏεγγγ¨γεΏ
θ¦γ§γοΌ
function getSnapshot() {
return navigator.onLine;
}欑γ«γsubscribe ι’ζ°γοΏ½?οΏ½θ£
γγεΏ
θ¦γγγγΎγγδΎγγ°γnavigator.onLine γε€εγγγ¨γγγ©γ¦γΆγ― window γͺγγΈγ§γ―γδΈγ§ online γγγ³ offline γ¨γγγ€γγ³γγηΊη«γγΎγγγγγε―ΎεΏγγγ€γγ³γγ« callback εΌζ°γη»ι²γγγγγθ§£ι€γγι’ζ°γθΏγεΏ
θ¦γγγγΎγοΌ
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}γγγ§ React γ―γε€ι¨οΏ½? navigator.onLine API γγε€γθͺγΏεγζΉζ³γ¨γγοΏ½?ε€ζ΄γ«γ΅γγΉγ―γ©γ€γγγζΉζ³γη₯γγγ¨γγ§γγΎγγγγγγ―γΌγ―γγγγγ€γΉγεζγγγ¨γγ³γ³γγΌγγ³γγεεΏγγ¦εγ¬γ³γγΌγγγγγ¨γ«ζ³¨οΏ½?γγ¦γγ γγοΌ
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); }; }
γγΈγγ―γγ«γΉγΏγ γγγ―γ«ζ½εΊγγ
ιεΈΈγuseSyncExternalStore γη΄ζ₯γ³γ³γγΌγγ³γε
γ«θ¨θΏ°γγγγ¨γ―γγγΎγγγ代γγγ«γθͺεθͺθΊ«οΏ½?γ«γΉγΏγ γγγ―γγεΌγ³εΊγγγ¨γδΈθ¬ηγ§γγγγγ«γγγη°γͺγγ³γ³γγΌγγ³γγγεγε€ι¨γΉγγ’γδ½Ώη¨γ§γγΎγγ
δΎγγ°γγοΏ½?γ«γΉγΏγ useOnlineStatus γγγ―γ―γγγγ―γΌγ―γγͺγ³γ©γ€γ³γ§γγγγ©γγγθΏ½θ·‘γγΎγοΌ
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
// ...
}
function subscribe(callback) {
// ...
}γγγ§γη°γͺγγ³γ³γγΌγγ³γγγεΊζ¬ηγͺοΏ½?οΏ½θ£
γηΉ°γθΏγγγ¨γͺγ useOnlineStatus γεΌγ³εΊγγγγγ«γͺγγΎγγοΌ
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 /> </> ); }
γ΅γΌγγΌγ¬γ³γγͺγ³γ°οΏ½?γ΅γγΌγγθΏ½ε γγ
React γ’γγͺγγ΅γΌγγ¬γ³γγͺγ³γ°γδ½Ώη¨γγ¦γγε ΄εγReact γ³γ³γγΌγγ³γγ―εζ HTML γηζγγγγγ«γγ©γ¦γΆη°ε’ε€γ§γοΏ½?οΏ½θ‘γγγΎγγγγγ«γγγε€ι¨γΉγγ’γΈοΏ½?ζ₯ηΆγ«ι’γγγγγ€γοΏ½?θͺ²ι‘γηγγΎγγ
- γγ©γ¦γΆε°η¨οΏ½? API γ«ζ₯ηΆγγ¦γγε ΄εγγγγ―γ΅γΌγδΈγ§γ―εε¨γγͺγγγεδ½γγΎγγγ
- γ΅γΌγγγΌγγ£οΏ½?γγΌγΏγΉγγ’γ«ζ₯ηΆγγ¦γγε ΄εγγ΅γΌγγ¨γ―γ©γ€γ’γ³γιγ§γοΏ½?γγΌγΏγδΈθ΄γγγεΏ θ¦γγγγΎγγ
γγγοΏ½?ει‘γθ§£ζ±Ίγγγγγ«γuseSyncExternalStore γ« getServerSnapshot ι’ζ°γ第 3 εΌζ°γ¨γγ¦ζΈ‘γγΎγοΌ
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isOnline;
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true; // Always show "Online" for server-generated HTML
}
function subscribe(callback) {
// ...
}getServerSnapshot ι’ζ°γ― getSnapshot γ¨δΌΌγ¦γγΎγγγδ»₯δΈοΏ½? 2 γ€οΏ½?ηΆζ³γ§οΏ½?γΏοΏ½?οΏ½θ‘γγγΎγοΌ
- γ΅γΌγδΈγ§γHTML γηζγγιγ«οΏ½?οΏ½θ‘γγγγ
- γ―γ©γ€γ’γ³γδΈγ§γReact γγ΅γΌγ HTML γγ€γ³γΏγ©γ―γγ£γγ«γγγ¨γγγ€γΎγγγ€γγ¬γΌγ·γ§γ³δΈγ«οΏ½?οΏ½θ‘γγγγ
γγγ«γγγγ’γγͺγγ€γ³γΏγ©γ―γγ£γγ«γͺγεγ«δ½Ώη¨γγγεζοΏ½?γΉγγγγ·γ§γγε€γζοΏ½?οΏ½γ§γγΎγγγ΅γΌγγ¬γ³γγͺγ³γ°δΈγ«ζε³οΏ½?γγεζε€γεε¨γγͺγε ΄εγ―γγοΏ½?εΌζ°γηη₯γγ¦γεΌ·εΆηγ«γ―γ©γ€γ’γ³γγ§γ¬γ³γγΌγγγγγ«γγΎγγ
γγ©γγ«γ·γ₯γΌγγ£γ³γ°
βThe result of getSnapshot should be cachedβ γ¨γγγ¨γ©γΌγεΊγ
γοΏ½?γ¨γ©γΌγ―γgetSnapshot ι’ζ°γεΌγ°γγγγ³γ«ζ°γγγͺγγΈγ§γ―γγθΏγγ¦γγγγ¨γζε³γγΎγγδΎγγ°οΌ
function getSnapshot() {
// π΄ Do not return always different objects from getSnapshot
return {
todos: myStore.todos
};
}getSnapshot οΏ½?θΏγε€γεεγ¨η°γͺγε ΄εγReact γ―γ³γ³γγΌγγ³γγεγ¬γ³γγΌγγΎγγγοΏ½?γγγεΈΈγ«η°γͺγε€γθΏγγ¨η‘ιγ«γΌγγ«ε
₯γγγοΏ½?γ¨γ©γΌγηΊηγγΎγγ
getSnapshot γͺγγΈγ§γ―γγ―γοΏ½?οΏ½ιγ«δ½γγε€ζ΄γγγε ΄εγ«οΏ½?γΏγε₯οΏ½?γͺγγΈγ§γ―γγθΏγεΏ
θ¦γγγγΎγγγΉγγ’γ«γ€γγ₯γΌγΏγγ«γͺγγΌγΏγε«γΎγγ¦γγε ΄εγ―γγοΏ½?γγΌγΏγη΄ζ₯θΏγγγ¨γγ§γγΎγοΌ
function getSnapshot() {
// β
You can return immutable data
return myStore.todos;
}γΉγγ’γγΌγΏγγγ₯γΌγΏγγ«γͺε ΄εγgetSnapshot ι’ζ°γ―γοΏ½?γ€γγ₯γΌγΏγγ«γͺγΉγγγγ·γ§γγγθΏγεΏ
θ¦γγγγΎγγγ€γΎγγζ°γγγͺγγΈγ§γ―γγδ½ζγγεΏ
θ¦γ―γγγΎγγγζ―εδ½ζγγ¦γ―γγγͺγγ¨γγγγ¨γ§γγγοΏ½?代γγγ«γζεΎγ«θ¨οΏ½?οΏ½γγγγΉγγγγ·γ§γγγδΏεγγ¦γγγγΉγγ’ε
οΏ½?γγΌγΏγε€ζ΄γγγ¦γγͺγε ΄εγ―εεγ¨εγγΉγγγγ·γ§γγγθΏγγγγ«γγΎγγγγ₯γΌγΏγγ«γͺγγΌγΏγε€ζ΄γγγγγ©γγγε€ζγγζΉζ³γ―γγγ₯γΌγΏγγ«γͺγΉγγ’γ«γγ£γ¦η°γͺγγΎγγ
subscribe γζ―γ¬γ³γγΌγγ¨γ«εΌγ³εΊγγγ
γοΏ½? subscribe ι’ζ°γ―γ³γ³γγΌγγ³γοΏ½?ε
ι¨γ§οΏ½?οΏ½ηΎ©γγγ¦γγγγγεγ¬γ³γγΌγγγγ³γ«η°γͺγ£γε€γ«γͺγγΎγοΌ
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// π© Always a different function, so React will resubscribe on every re-render
function subscribe() {
// ...
}
// ...
}React γ―γεγ¬γ³γγΌιγ§η°γͺγ subscribe ι’ζ°γζΈ‘γγ¨γγΉγγ’γ«εγ΅γγΉγ―γ©γ€γγγΎγγγγγγγγ©γΌγγ³γΉοΏ½?ει‘γεΌγθ΅·γγγεγ΅γγΉγ―γ©γ€γγιΏγγγε ΄εγ―γsubscribe ι’ζ°γε€ι¨γ«η§»εγγ¦γγ γγοΌ
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
// β
Always the same function, so React won't need to resubscribe
function subscribe() {
// ...
}γγγγ―γsubscribe γ useCallback γ§γ©γγγγγγ¨γ§γεΌζ°γε€ζ΄γγγγ¨γοΏ½?γΏεγ΅γγΉγ―γ©γ€γγγγγ¨γγ§γγΎγοΌ
function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// β
Same function as long as userId doesn't change
const subscribe = useCallback(() => {
// ...
}, [userId]);
// ...
}