UNPKG

use-db

Version:

IndexedDB React hook that mimics useState API supporting optimistic updates

137 lines (136 loc) 4.16 kB
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore, } from "react"; import { DbStorage } from "local-db-storage"; const dbStorage = new DbStorage({ name: "node_modules/use-db", }); const syncData = new Map(); export default function useDb(key, options) { const [defaultValue] = useState(options?.defaultValue); return useDbStorage(key, defaultValue, options?.optimistic ?? true); } function useDbStorage(key, defaultValue, optimistic) { const [ready] = useState(() => createReady()); const value = useSyncExternalStore( // useSyncExternalStore.subscribe useCallback((onStoreChange) => { const onChange = (localKey) => { if (key === localKey) { onStoreChange(); } }; callbacks.add(onChange); return () => { callbacks.delete(onChange); }; }, [key]), // useSyncExternalStore.getSnapshot () => { return syncData.has(key) ? syncData.get(key) : defaultValue; }, // useSyncExternalStore.getServerSnapshot () => defaultValue); const setState = useCallback((newValue) => { const set = () => { const hasPrev = syncData.has(key); const prev = syncData.has(key) ? syncData.get(key) : defaultValue; const next = newValue instanceof Function ? newValue(prev) : newValue; if (optimistic) { syncData.set(key, next); triggerCallbacks(key); return dbStorage.setItem(key, next).catch(() => { if (hasPrev) { syncData.set(key, prev); } else { syncData.delete(key); } }); } else { return dbStorage .setItem(key, next) .then(() => { syncData.set(key, next); triggerCallbacks(key); }) .catch(() => { }); } }; if (!ready.is) { return ready.promise.then(() => set()); } return set(); }, [key]); const removeItem = useCallback(() => { const prev = syncData.get(key); const hasPrev = syncData.has(key); if (optimistic) { syncData.delete(key); triggerCallbacks(key); return dbStorage.removeItem(key).catch(() => { if (hasPrev) { syncData.set(key, prev); triggerCallbacks(key); } }); } else { return dbStorage .removeItem(key) .then(() => { syncData.delete(key); triggerCallbacks(key); }) .catch(() => { }); } }, [key]); const [, forceRender] = useState(0); useEffect(() => { if (ready.is) return; let disposed = false; dbStorage .getItem(key) .then((value) => { ready.resolve(); if (!disposed && syncData.get(key) !== value) { syncData.set(key, value); forceRender((prev) => prev + 1); } }) .catch(() => { }) .finally(() => ready.resolve()); return () => { disposed = true; }; }); return useMemo(() => [value, setState, removeItem], [value, setState, removeItem]); } // notifies all instances using the same `key` to update const callbacks = new Set(); function triggerCallbacks(key) { for (const callback of [...callbacks]) { callback(key); } } function createReady() { let resolveFn; let completed = false; const promise = new Promise((resolve) => { resolveFn = () => { completed = true; resolve(); }; }); return { promise, resolve: resolveFn, get is() { return completed; }, }; }