En djupdykning i Reacts useSyncExternalStore-hook för synkronisering av externa datalager, inklusive implementeringsstrategier och prestandaövervÀganden.
React useSyncExternalStore: BemÀstra synkronisering med externa stores
I moderna React-applikationer Àr det avgörande att hantera state effektivt. Medan React erbjuder inbyggda lösningar för state-hantering som useState och useReducer, krÀver integration med externa datakÀllor eller tredjepartsbibliotek för state-hantering ett mer sofistikerat tillvÀgagÄngssÀtt. Det Àr hÀr useSyncExternalStore kommer in i bilden.
Vad Àr useSyncExternalStore?
useSyncExternalStore Àr en React-hook som introducerades i React 18 och lÄter dig prenumerera pÄ och lÀsa frÄn externa datakÀllor pÄ ett sÀtt som Àr kompatibelt med concurrent rendering. Detta Àr sÀrskilt viktigt nÀr man hanterar data som inte direkt hanteras av React, sÄsom:
- Tredjepartsbibliotek för state-hantering: Redux, Zustand, Jotai, etc.
- WebblÀsar-API:er:
localStorage,IndexedDB, etc. - Externa datakÀllor: Server-sent events, WebSockets, etc.
Innan useSyncExternalStore kunde synkronisering av externa stores leda till "tearing" och inkonsekvenser, sÀrskilt med Reacts concurrent rendering-funktioner. Denna hook löser dessa problem genom att erbjuda ett standardiserat och högpresterande sÀtt att ansluta externa data till dina React-komponenter.
Varför anvÀnda useSyncExternalStore? Fördelar och vinster
Att anvÀnda useSyncExternalStore erbjuder flera viktiga fördelar:
- SÀkerhet vid samtidighet: Garanterar att din komponent alltid visar en konsekvent vy av det externa store, Àven under samtidiga renderingar. Detta förhindrar "tearing"-problem dÀr delar av ditt UI kan visa inkonsekvent data.
- Prestanda: Optimerad för prestanda, vilket minimerar onödiga omrenderingar. Den utnyttjar Reacts interna mekanismer för att effektivt prenumerera pÄ Àndringar och uppdatera komponenten endast nÀr det behövs.
- Standardiserat API: Ger ett konsekvent och förutsÀgbart API för att interagera med externa stores, oavsett den underliggande implementationen.
- Minskad boilerplate-kod: Förenklar processen att ansluta till externa stores, vilket minskar mÀngden anpassad kod du behöver skriva.
- Kompatibilitet: Fungerar sömlöst med ett brett utbud av externa datakÀllor och bibliotek för state-hantering.
Hur useSyncExternalStore fungerar: En djupdykning
Hooken useSyncExternalStore tar tre argument:
subscribe(callback: () => void): () => void: En funktion som registrerar en callback för att meddelas nÀr det externa store Àndras. Den ska returnera en funktion för att avprenumerera. Det Àr sÄ hÀr React fÄr veta nÀr det finns ny data i store.getSnapshot(): T: En funktion som returnerar en ögonblicksbild (snapshot) av datan frÄn det externa store. Denna snapshot bör vara ett enkelt, oförÀnderligt vÀrde som React kan anvÀnda för att avgöra om datan har Àndrats.getServerSnapshot?(): T(Valfri): En funktion som returnerar den initiala snapshoten av datan pÄ servern. Detta anvÀnds för server-side rendering (SSR) för att sÀkerstÀlla konsekvens mellan server och klient. Om den inte tillhandahÄlls kommer React att anvÀndagetSnapshot()under serverrendering, vilket kanske inte Àr idealiskt i alla scenarier.
HÀr Àr en genomgÄng av hur dessa argument samverkar:
- NĂ€r komponenten monteras anropar
useSyncExternalStoresubscribe-funktionen för att registrera en callback. - NÀr det externa store Àndras, anropas den callback som registrerades via
subscribe. - Callbacken meddelar React att komponenten behöver renderas om.
- Under renderingen anropar
useSyncExternalStoregetSnapshotför att hÀmta den senaste datan frÄn det externa store. - React jÀmför den nuvarande snapshoten med den föregÄende. Om de skiljer sig Ät uppdateras komponenten med den nya datan.
- NĂ€r komponenten avmonteras anropas avprenumerationsfunktionen som returnerades av
subscribeför att förhindra minneslÀckor.
GrundlÀggande implementeringsexempel: Integration med localStorage
LÄt oss illustrera hur man anvÀnder useSyncExternalStore med ett enkelt exempel: att lÀsa och skriva ett vÀrde till localStorage.
import { useSyncExternalStore } from 'react';
function getLocalStorageItem(key: string): string | null {
try {
return localStorage.getItem(key);
} catch (error) {
console.error("Error accessing localStorage:", error);
return null; // Hantera potentiella fel som att `localStorage` inte Àr tillgÀngligt.
}
}
function useLocalStorage(key: string): [string | null, (value: string) => void] {
const subscribe = (callback: () => void) => {
window.addEventListener('storage', callback);
return () => window.removeEventListener('storage', callback);
};
const getSnapshot = () => getLocalStorageItem(key);
const serverSnapshot = () => null; // Eller ett standardvÀrde om det Àr lÀmpligt för din SSR-setup
const value = useSyncExternalStore(subscribe, getSnapshot, serverSnapshot);
const setValue = (newValue: string) => {
try {
localStorage.setItem(key, newValue);
// Skicka ett storage-event pÄ det aktuella fönstret för att trigga uppdateringar i andra flikar.
window.dispatchEvent(new StorageEvent('storage', {
key: key,
newValue: newValue,
storageArea: localStorage,
} as StorageEventInit));
} catch (error) {
console.error("Error setting localStorage:", error);
}
};
return [value, setValue];
}
function MyComponent() {
const [name, setName] = useLocalStorage('name');
return (
<div>
<p>Hej, {name || 'vÀrlden'}</p>
<input
type="text"
value={name || ''}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
export default MyComponent;
Förklaring:
getLocalStorageItem: En hjÀlpfunktion för att sÀkert hÀmta vÀrdet frÄnlocalStorageoch hantera potentiella fel.useLocalStorage: En anpassad hook som kapslar in logiken för att interagera medlocalStoragemed hjÀlp avuseSyncExternalStore.subscribe: Lyssnar pÄ'storage'-eventet, som utlöses nÀrlocalStorageÀndras i en annan flik eller ett annat fönster. Kritiskt nog skickar vi ett storage-event efter att ha satt ett nytt vÀrde för att korrekt trigga uppdateringar i *samma* fönster.getSnapshot: Returnerar det aktuella vÀrdet frÄnlocalStorage.serverSnapshot: Returnerarnull(eller ett standardvÀrde) för server-side rendering.setValue: Uppdaterar vÀrdet ilocalStorageoch skickar ett storage-event för att signalera andra flikar.MyComponent: En enkel komponent som anvÀnderuseLocalStorage-hooken för att visa och uppdatera ett namn.
Viktiga övervÀganden för localStorage:
- Felhantering: Omslut alltid Ätkomst till
localStorageitry...catch-block för att hantera potentiella fel, som nÀrlocalStorageÀr inaktiverat eller otillgÀngligt (t.ex. i privat surflÀge). - Storage Events:
'storage'-eventet utlöses endast nÀrlocalStorageÀndras i en *annan* flik eller ett annat fönster, inte i samma fönster. DÀrför skickar vi ett nyttStorageEventmanuellt efter att ha satt ett vÀrde. - Dataserialisering:
localStoragelagrar endast strÀngar. Du kan behöva serialisera och deserialisera komplexa datastrukturer medJSON.stringifyochJSON.parse. - SÀkerhet: Var medveten om vilken data du lagrar i
localStorage, eftersom den Àr tillgÀnglig för JavaScript-kod pÄ samma domÀn. KÀnslig information bör inte lagras ilocalStorage.
Avancerade anvÀndningsfall och exempel
1. Integration med Zustand (eller annat bibliotek för state-hantering)
Att integrera useSyncExternalStore med ett globalt bibliotek för state-hantering som Zustand Àr ett vanligt anvÀndningsfall. HÀr Àr ett exempel:
import { useSyncExternalStore } from 'react';
import { create } from 'zustand';
interface BearState {
bears: number
increase: (by: number) => void
}
const useStore = create<BearState>((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by }))
}))
function BearCounter() {
const bears = useSyncExternalStore(
useStore.subscribe,
useStore.getState,
() => ({ bears: 0, increase: () => {} }) // Server-snapshot, ange ett standard-state
).bears
return <h1>{bears} björnar hÀr i krokarna!</h1>
}
function Controls() {
const increase = useStore(state => state.increase)
return (<button onClick={() => increase(1)}>en björn</button>)
}
export { BearCounter, Controls }
Förklaring:
- Vi anvÀnder Zustand för global state-hantering.
useStore.subscribe: Denna funktion prenumererar pÄ Zustand store och kommer att utlösa omrenderingar nÀr storets state Àndras.useStore.getState: Denna funktion returnerar det nuvarande statet för Zustand store.- Den tredje parametern tillhandahÄller ett standard-state för server-side rendering (SSR), vilket sÀkerstÀller att komponenten renderas korrekt pÄ servern innan den klient-sidiga JavaScript-koden tar över.
- Komponenten hÀmtar antalet björnar med
useSyncExternalStoreoch renderar det. Controls-komponenten visar hur man anvÀnder en setter frÄn Zustand.
2. Integration med Server-Sent Events (SSE)
useSyncExternalStore kan anvÀndas för att effektivt uppdatera komponenter baserat pÄ realtidsdata frÄn en server med hjÀlp av Server-Sent Events (SSE).
import { useSyncExternalStore, useState, useEffect, useCallback } from 'react';
function useSSE(url: string) {
const [data, setData] = useState(null);
const [eventSource, setEventSource] = useState(null);
useEffect(() => {
const newEventSource = new EventSource(url);
setEventSource(newEventSource);
newEventSource.onmessage = (event) => {
try {
const parsedData = JSON.parse(event.data);
setData(parsedData);
} catch (error) {
console.error("Error parsing SSE data:", error);
}
};
newEventSource.onerror = (error) => {
console.error("SSE error:", error);
};
return () => {
newEventSource.close();
setEventSource(null);
};
}, [url]);
const subscribe = useCallback((callback: () => void) => {
if (eventSource) {
eventSource.addEventListener('message', callback);
}
return () => {
if (eventSource) {
eventSource.removeEventListener('message', callback);
}
};
}, [eventSource]);
const getSnapshot = useCallback(() => data, [data]);
const serverSnapshot = useCallback(() => null, []);
const value = useSyncExternalStore(subscribe, getSnapshot, serverSnapshot);
return value;
}
function RealTimeDataComponent() {
const realTimeData = useSSE('/api/sse'); // ErsÀtt med din SSE-endpoint
if (!realTimeData) {
return <p>Laddar...</p>;
}
return <div><p>Realtidsdata: {JSON.stringify(realTimeData)}</p></div>;
}
export default RealTimeDataComponent;
Förklaring:
useSSE: En anpassad hook som upprÀttar en SSE-anslutning till en given URL.subscribe: LÀgger till en event listener tillEventSource-objektet för att meddelas om nya meddelanden frÄn servern. Den anvÀnderuseCallbackför att sÀkerstÀlla att callback-funktionen inte Äterskapas vid varje rendering.getSnapshot: Returnerar den senast mottagna datan frÄn SSE-strömmen.serverSnapshot: Returnerarnullför server-side rendering.RealTimeDataComponent: En komponent som anvÀnderuseSSE-hooken för att visa realtidsdata.
3. Integration med IndexedDB
Synkronisera React-komponenter med data lagrad i IndexedDB med hjÀlp av useSyncExternalStore.
import { useSyncExternalStore, useState, useEffect, useCallback } from 'react';
interface IDBData {
id: number;
name: string;
}
async function getAllData(): Promise {
return new Promise((resolve, reject) => {
const request = indexedDB.open('myDataBase', 1); // ErsÀtt med ditt databasnamn och version
request.onerror = (event) => {
console.error("IndexedDB open error:", event);
reject(event);
};
request.onsuccess = (event) => {
const db = (event.target as IDBRequest).result as IDBDatabase;
const transaction = db.transaction(['myDataStore'], 'readonly'); // ErsÀtt med namnet pÄ ditt store
const objectStore = transaction.objectStore('myDataStore');
const getAllRequest = objectStore.getAll();
getAllRequest.onsuccess = (event) => {
const data = (event.target as IDBRequest).result as IDBData[];
resolve(data);
};
getAllRequest.onerror = (event) => {
console.error("IndexedDB getAll error:", event);
reject(event);
};
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBRequest).result as IDBDatabase;
db.createObjectStore('myDataStore', { keyPath: 'id' });
};
});
}
function useIndexedDBData(): IDBData[] | null {
const [data, setData] = useState(null);
const [dbInitialized, setDbInitialized] = useState(false);
useEffect(() => {
const initDB = async () => {
try{
await getAllData();
setDbInitialized(true);
} catch (e) {
console.error("IndexedDB initialization failed", e);
}
}
initDB();
}, []);
const subscribe = useCallback((callback: () => void) => {
// AnvÀnd debounce pÄ callbacken för att förhindra överdrivet mÄnga omrenderingar.
let timeoutId: NodeJS.Timeout;
const debouncedCallback = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(callback, 50); // Justera debounce-fördröjningen vid behov
};
const handleVisibilityChange = () => {
// HÀmta data igen nÀr fliken blir synlig
if (document.visibilityState === 'visible') {
debouncedCallback();
}
};
window.addEventListener('focus', debouncedCallback);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
window.removeEventListener('focus', debouncedCallback);
document.removeEventListener('visibilitychange', handleVisibilityChange);
clearTimeout(timeoutId);
};
}, []);
const getSnapshot = useCallback(() => {
// HÀmta den senaste datan frÄn IndexedDB varje gÄng getSnapshot anropas
getAllData().then(newData => setData(newData));
return data;
}, [data]);
const serverSnapshot = useCallback(() => null, []);
return useSyncExternalStore(subscribe, getSnapshot, serverSnapshot);
}
function IndexedDBComponent() {
const data = useIndexedDBData();
if (!data) {
return <p>Laddar data frÄn IndexedDB...</p>;
}
return (
<div>
<h2>Data frÄn IndexedDB:</h2>
<ul>
{data.map((item) => (
<li key={item.id}>{item.name} (ID: {item.id})</li>
))}
</ul>
</div>
);
}
export default IndexedDBComponent;
Förklaring:
getAllData: En asynkron funktion som hÀmtar all data frÄn IndexedDB store.useIndexedDBData: En anpassad hook som anvÀnderuseSyncExternalStoreför att prenumerera pÄ Àndringar i IndexedDB.subscribe: SÀtter upp lyssnare för synlighets- och fokusÀndringar för att uppdatera datan frÄn IndexedDB och anvÀnder en debounce-funktion för att undvika överdrivna uppdateringar.getSnapshot: HÀmtar den aktuella snapshoten genom att anropa `getAllData()` och returnerar sedan `data` frÄn state.serverSnapshot: Returnerarnullför server-side rendering.IndexedDBComponent: En komponent som visar data frÄn IndexedDB.
Viktiga övervÀganden för IndexedDB:
- Asynkrona operationer: Interaktioner med IndexedDB Àr asynkrona, sÄ du mÄste hantera den asynkrona naturen av datahÀmtning och uppdateringar noggrant.
- Felhantering: Implementera robust felhantering för att elegant hantera potentiella problem med databasÄtkomst, sÄsom att databasen inte hittas eller behörighetsfel.
- Databasversionering: Hantera databasversioner noggrant med
onupgradeneeded-eventet för att sÀkerstÀlla datakompatibilitet nÀr din applikation utvecklas. - Prestanda: IndexedDB-operationer kan vara relativt lÄngsamma, sÀrskilt för stora datamÀngder. Optimera frÄgor och indexering för att förbÀttra prestandan.
PrestandaövervÀganden
Ăven om useSyncExternalStore Ă€r optimerad för prestanda finns det fortfarande nĂ„gra saker att tĂ€nka pĂ„:
- Minimera snapshot-Ă€ndringar: Se till att
getSnapshot-funktionen endast returnerar en ny snapshot nĂ€r datan faktiskt har Ă€ndrats. Undvik att skapa nya objekt eller arrayer i onödan. ĂvervĂ€g att anvĂ€nda memoization-tekniker för att optimera skapandet av snapshots. - Batcha uppdateringar: Om möjligt, batcha uppdateringar till det externa store för att minska antalet omrenderingar. Om du till exempel uppdaterar flera egenskaper i store, försök att uppdatera dem alla i en enda transaktion.
- Debouncing/Throttling: Om det externa store Àndras ofta, övervÀg att anvÀnda debouncing eller throttling pÄ uppdateringarna till React-komponenten. Detta kan förhindra överdrivna omrenderingar och förbÀttra prestandan. Detta Àr sÀrskilt anvÀndbart med volatila stores som Àndringar i webblÀsarfönstrets storlek.
- Ytlig jÀmförelse: Se till att du returnerar primitiva vÀrden eller oförÀnderliga objekt i
getSnapshotsÄ att React snabbt kan avgöra om datan har Àndrats med en ytlig jÀmförelse. - Villkorliga uppdateringar: I fall dÀr det externa store Àndras ofta men din komponent bara behöver reagera pÄ vissa Àndringar, övervÀg att implementera villkorliga uppdateringar inom `subscribe`-funktionen för att undvika onödiga omrenderingar.
Vanliga fallgropar och felsökning
- Tearing-problem: Om du fortfarande upplever tearing-problem efter att ha anvÀnt
useSyncExternalStore, dubbelkolla att dingetSnapshot-funktion returnerar en konsekvent vy av datan och attsubscribe-funktionen korrekt meddelar React om Àndringar. Se till att du inte muterar datan direkt inutigetSnapshot-funktionen. - OÀndliga loopar: En oÀndlig loop kan uppstÄ om
getSnapshot-funktionen alltid returnerar ett nytt vÀrde, Àven nÀr datan inte har Àndrats. Detta kan hÀnda om du skapar nya objekt eller arrayer i onödan. Se till att du returnerar samma vÀrde om datan inte har Àndrats. - Saknad server-side rendering: Om du anvÀnder server-side rendering, se till att tillhandahÄlla en
getServerSnapshot-funktion för att sÀkerstÀlla att komponenten renderas korrekt pÄ servern. Denna funktion bör returnera det initiala statet för det externa store. - Felaktig avprenumeration: Se alltid till att du korrekt avprenumererar frÄn det externa store i funktionen som returneras av
subscribe. Att inte göra det kan leda till minneslÀckor. - Felaktig anvÀndning med Concurrent Mode: Se till att ditt externa store Àr kompatibelt med Concurrent Mode. Undvik att göra mutationer i det externa store medan React renderar. Mutationer bör vara synkrona och förutsÀgbara.
Slutsats
useSyncExternalStore Àr ett kraftfullt verktyg för att synkronisera React-komponenter med externa datalager. Genom att förstÄ hur det fungerar och följa bÀsta praxis kan du sÀkerstÀlla att dina komponenter visar konsekvent och uppdaterad data, Àven i komplexa scenarier med concurrent rendering. Denna hook förenklar integrationen med olika datakÀllor, frÄn tredjepartsbibliotek för state-hantering till webblÀsar-API:er och realtidsdataströmmar, vilket leder till mer robusta och högpresterande React-applikationer. Kom ihÄg att alltid hantera potentiella fel, optimera prestanda och noggrant hantera prenumerationer för att undvika vanliga fallgropar.