use-db
Version:
IndexedDB React hook that mimics useState API supporting optimistic updates
137 lines (136 loc) • 4.16 kB
JavaScript
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;
},
};
}