Skip to content

Commit b2b23f6

Browse files
committed
🐛 cannot read state from IndexDB after page refresh, resolve #1
1 parent b5cb87e commit b2b23f6

File tree

2 files changed

+145
-38
lines changed

2 files changed

+145
-38
lines changed

Diff for: index.ts

+87-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { SetStateAction } from "react";
2-
import { useCallback, useMemo, useState, useSyncExternalStore } from "react";
2+
import {
3+
useCallback,
4+
useEffect,
5+
useMemo,
6+
useState,
7+
useSyncExternalStore,
8+
} from "react";
39
import { DbStorage } from "local-db-storage";
410

511
const dbStorage = new DbStorage({
@@ -45,6 +51,7 @@ function useDbStorage<T>(
4551
defaultValue: T | undefined,
4652
optimistic: boolean,
4753
): DbState<T | undefined> {
54+
const [ready] = useState(() => createReady());
4855
const value = useSyncExternalStore(
4956
// useSyncExternalStore.subscribe
5057
useCallback(
@@ -75,36 +82,46 @@ function useDbStorage<T>(
7582

7683
const setState = useCallback(
7784
(newValue: SetStateAction<T | undefined>): Promise<void> => {
78-
const hasPrev = syncData.has(key);
79-
const prev = syncData.has(key)
80-
? (syncData.get(key) as T | undefined)
81-
: defaultValue;
82-
const next =
83-
newValue instanceof Function ? newValue(prev) : newValue;
84-
if (optimistic) {
85-
syncData.set(key, next);
86-
triggerCallbacks(key);
87-
return dbStorage.setItem(key, next).catch(() => {
88-
if (hasPrev) {
89-
syncData.set(key, prev);
90-
} else {
91-
syncData.delete(key);
92-
}
93-
triggerCallbacks(key);
94-
});
95-
} else {
96-
return dbStorage.setItem(key, next).then(() => {
85+
const set = (): Promise<void> => {
86+
const hasPrev = syncData.has(key);
87+
const prev = syncData.has(key)
88+
? (syncData.get(key) as T | undefined)
89+
: defaultValue;
90+
const next =
91+
newValue instanceof Function ? newValue(prev) : newValue;
92+
93+
if (optimistic) {
9794
syncData.set(key, next);
9895
triggerCallbacks(key);
99-
});
96+
return dbStorage.setItem(key, next).catch(() => {
97+
if (hasPrev) {
98+
syncData.set(key, prev);
99+
} else {
100+
syncData.delete(key);
101+
}
102+
});
103+
} else {
104+
return dbStorage
105+
.setItem(key, next)
106+
.then(() => {
107+
syncData.set(key, next);
108+
triggerCallbacks(key);
109+
})
110+
.catch(() => {});
111+
}
112+
};
113+
if (!ready.is) {
114+
return ready.promise.then(() => set())
100115
}
116+
return set();
101117
},
102118
[key],
103119
);
104120

105121
const removeItem = useCallback(() => {
106122
const prev = syncData.get(key);
107123
const hasPrev = syncData.has(key);
124+
108125
if (optimistic) {
109126
syncData.delete(key);
110127
triggerCallbacks(key);
@@ -115,13 +132,36 @@ function useDbStorage<T>(
115132
}
116133
});
117134
} else {
118-
return dbStorage.removeItem(key).then(() => {
119-
syncData.delete(key);
120-
triggerCallbacks(key);
121-
});
135+
return dbStorage
136+
.removeItem(key)
137+
.then(() => {
138+
syncData.delete(key);
139+
triggerCallbacks(key);
140+
})
141+
.catch(() => {});
122142
}
123143
}, [key]);
124144

145+
const [, forceRender] = useState(0);
146+
useEffect(() => {
147+
if (ready.is) return;
148+
let disposed = false;
149+
dbStorage
150+
.getItem(key)
151+
.then((value) => {
152+
ready.resolve();
153+
if (!disposed && syncData.get(key) !== value) {
154+
syncData.set(key, value);
155+
forceRender((prev) => prev + 1);
156+
}
157+
})
158+
.catch(() => {})
159+
.finally(() => ready.resolve());
160+
return () => {
161+
disposed = true;
162+
};
163+
});
164+
125165
return useMemo(
126166
() => [value, setState, removeItem],
127167
[value, setState, removeItem],
@@ -135,3 +175,25 @@ function triggerCallbacks(key: string): void {
135175
callback(key);
136176
}
137177
}
178+
179+
function createReady(): {
180+
promise: Promise<void>;
181+
resolve: () => void;
182+
is: boolean;
183+
} {
184+
let resolveFn: () => void;
185+
let completed = false;
186+
const promise = new Promise<void>((resolve) => {
187+
resolveFn = () => {
188+
completed = true;
189+
resolve();
190+
};
191+
});
192+
return {
193+
promise,
194+
resolve: resolveFn!,
195+
get is() {
196+
return completed;
197+
},
198+
};
199+
}

Diff for: test.ts

+58-13
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import "fake-indexeddb/auto";
22

33
import { describe, expect, test, vi } from "vitest";
44
import { act, renderHook } from "@testing-library/react";
5-
import useDb, { type UseDbOptions } from "./index.js";
5+
import useDb from "./index.js";
66
import { DbStorage } from "local-db-storage";
77

88
describe("use-db", () => {
@@ -31,14 +31,16 @@ describe("use-db", () => {
3131
expect(todos).toStrictEqual(["first", "second"]);
3232
});
3333

34-
test("updates state", () => {
34+
test("updates state", async () => {
3535
const key = crypto.randomUUID();
3636
const { result } = renderHook(() =>
3737
useDb(key, {
3838
defaultValue: ["first", "second"],
3939
}),
4040
);
4141

42+
await wait(5);
43+
4244
act(() => {
4345
const setTodos = result.current[1];
4446
setTodos(["third", "forth"]);
@@ -48,14 +50,16 @@ describe("use-db", () => {
4850
expect(todos).toStrictEqual(["third", "forth"]);
4951
});
5052

51-
test("updates state with callback function", () => {
53+
test("updates state with callback function", async () => {
5254
const key = crypto.randomUUID();
5355
const { result } = renderHook(() =>
5456
useDb(key, {
5557
defaultValue: ["first", "second"],
5658
}),
5759
);
5860

61+
await wait(5);
62+
5963
act(() => {
6064
const setTodos = result.current[1];
6165

@@ -74,10 +78,12 @@ describe("use-db", () => {
7478
}),
7579
);
7680

81+
await wait(5);
82+
7783
{
78-
await act(() => {
84+
act(() => {
7985
const setTodos = result.current[1];
80-
return setTodos(["third", "forth"]);
86+
setTodos(["third", "forth"]);
8187
});
8288
const [todos] = result.current;
8389
expect(todos).toStrictEqual(["third", "forth"]);
@@ -93,14 +99,16 @@ describe("use-db", () => {
9399
}
94100
});
95101

96-
test("persists state across hook re-renders", () => {
102+
test("persists state across hook re-renders", async () => {
97103
const key = crypto.randomUUID();
98104
const { result, rerender } = renderHook(() =>
99105
useDb(key, {
100106
defaultValue: ["first", "second"],
101107
}),
102108
);
103109

110+
await wait(5);
111+
104112
act(() => {
105113
const setTodos = result.current[1];
106114
setTodos(["third", "fourth"]);
@@ -112,7 +120,7 @@ describe("use-db", () => {
112120
expect(todos).toStrictEqual(["third", "fourth"]);
113121
});
114122

115-
test("handles complex objects", () => {
123+
test("handles complex objects", async () => {
116124
const complexObject = {
117125
nested: { array: [1, 2, 3], value: "test" },
118126
};
@@ -121,6 +129,8 @@ describe("use-db", () => {
121129
useDb(key, { defaultValue: complexObject }),
122130
);
123131

132+
await wait(5);
133+
124134
const [storedObject] = result.current;
125135
expect(storedObject).toEqual(complexObject);
126136

@@ -138,10 +148,12 @@ describe("use-db", () => {
138148
});
139149
});
140150

141-
test("handles undefined as a valid state", () => {
151+
test("handles undefined as a valid state", async () => {
142152
const key = crypto.randomUUID();
143153
const { result } = renderHook(() => useDb(key));
144154

155+
await wait(5);
156+
145157
const [initialState] = result.current;
146158
expect(initialState).toBeUndefined();
147159

@@ -168,17 +180,21 @@ describe("use-db", () => {
168180
unmount();
169181
});
170182

171-
test("set state throws an error", () => {
183+
test("set state throws an error", async () => {
172184
const key = crypto.randomUUID();
173-
const { result } = renderHook(() => useDb(key));
185+
const hook = renderHook(() => useDb(key));
186+
187+
// no idea why this is needed.
188+
// otherwise, it throws "unhadled error -- Vitest caught 1 error during the test run."
189+
await wait(5);
174190

175191
vi.spyOn(DbStorage.prototype, "setItem").mockReturnValue(
176192
Promise.reject("QuotaExceededError"),
177193
);
178194

179-
act(() => {
180-
const setState = result.current[1];
181-
setState("defined");
195+
await act(() => {
196+
const [, setState] = hook.result.current;
197+
return setState("defined");
182198
});
183199
});
184200

@@ -227,6 +243,29 @@ describe("use-db", () => {
227243
const [number] = result.current;
228244
expect(number).toBe(2);
229245
});
246+
247+
// https://github.com/astoilkov/use-db/issues/1
248+
test("cannot read state from IndexDB after page refresh", async () => {
249+
const key = crypto.randomUUID();
250+
251+
const dbStorage = new DbStorage({
252+
name: "node_modules/use-db",
253+
});
254+
await dbStorage.setItem(key, ["first", "second"]);
255+
256+
const hook = renderHook(() => useDb(key));
257+
const todos = await vi.waitUntil(
258+
() => {
259+
const [todos] = hook.result.current;
260+
return todos;
261+
},
262+
{
263+
timeout: 100,
264+
interval: 10,
265+
},
266+
);
267+
expect(todos).toStrictEqual(["first", "second"]);
268+
});
230269
});
231270

232271
describe("non-optimistic", () => {
@@ -290,3 +329,9 @@ describe("use-db", () => {
290329
});
291330
});
292331
});
332+
333+
function wait(ms: number): Promise<void> {
334+
return new Promise((resolve) => {
335+
setTimeout(resolve, ms);
336+
});
337+
}

0 commit comments

Comments
 (0)