Problem
In development mode, React 18+ Strict Mode double-invokes effects to help detect side-effect issues. When a useEffect opens an IndexedDB connection (directly or via Dexie.js), the rapid mount-unmount-remount cycle causes the database connection to fail:
Error: Database not open. Call open() first.
at Dexie._handleError (dexie.js:1823:15)
at Table._trans (dexie.js:1456:24)
at Table.toArray (dexie.js:1512:18)
at loadCachedData (useLocalCache.ts:28:32)
The code that triggers this:
useEffect(() => {
const db = new Dexie("MyAppCache");
db.version(1).stores({ items: "++id, name" });
db.open().then(() => {
db.table("items").toArray().then(setItems);
});
return () => {
db.close(); // First mount: closes DB
};
// Second mount: opens new connection on stale/closing DB
}, []);
Solution
Use a ref to track the initialization state and prevent double-opening:
import { useEffect, useRef, useState } from "react";
import Dexie from "dexie";
const db = new Dexie("MyAppCache");
db.version(1).stores({ items: "++id, name" });
function useLocalCache() {
const [items, setItems] = useState<Item[]>([]);
const initialized = useRef(false);
useEffect(() => {
if (initialized.current) return;
initialized.current = true;
db.open().then(() => {
db.table("items").toArray().then(setItems);
});
return () => {
initialized.current = false;
db.close();
};
}, []);
return items;
}
The key changes are:
- Move the
Dexieinstance outside the effect so it is not recreated on each mount - Use a
useRefflag to skip the second initialization during the Strict Mode remount - Reset the flag in cleanup so subsequent real unmount/remount cycles still work
Why It Works
React Strict Mode calls effects twice in development: mount, unmount (cleanup), then mount again. IndexedDB connections are stateful resources -- closing a connection and immediately reopening it on the same database can fail because the close operation is asynchronous and may not complete before the reopen attempt. The ref guard ensures the database is only opened once, and the module-level Dexie instance persists across the Strict Mode remount cycle.
Context
- React 18+ Strict Mode in development only -- production builds run effects once
- Applies to any IndexedDB wrapper: Dexie.js, idb, localForage, or the raw IndexedDB API
- The same pattern applies to other stateful browser APIs like WebSocket, EventSource, or BroadcastChannel
- If using Dexie.js, version 4+ has improved handling of concurrent open/close but the ref guard is still recommended
- An alternative approach is using
useSyncExternalStorewith an external store that manages the DB lifecycle independently of React