IndexedDB has:
- Database
- Object Stores (like tables)
- Transactions
- Indexes
- Requests (async events)
Flow:
- Open database
- Create object stores (only in upgrade phase)
- Start transaction
- Perform read/write
- Handle success/error
Important:
Opening DB is async and version-controlled.
Step 1 — Creating a Simple IndexedDB Utility (Clean Way)
We’ll build a reusable wrapper first.
db.ts (Minimal Production-Ready Wrapper)
const DB_NAME = "MyAppDB";
const DB_VERSION = 1;
const STORE_NAME = "items";
export function openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = (event) => {
const db = request.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: "id" });
}
};
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
Step 2 — CRUD Operations
Add Item
export async function addItem(item: any) {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, "readwrite");
const store = transaction.objectStore(STORE_NAME);
const request = store.put(item);
request.onsuccess = () => resolve(true);
request.onerror = () => reject(request.error);
});
}
Get All Items
export async function getAllItems() {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, "readonly");
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
Step 3 — Using IndexedDB in React
Now we integrate into a React component properly.
Example: Offline Task List
import { useEffect, useState } from "react";
import { addItem, getAllItems } from "./db";
interface Task {
id: number;
title: string;
}
export default function App() {
const [tasks, setTasks] = useState<Task[]>([]);
const [input, setInput] = useState("");
useEffect(() => {
loadTasks();
}, []);
const loadTasks = async () => {
const stored = (await getAllItems()) as Task[];
setTasks(stored);
};
const handleAdd = async () => {
const newTask = {
id: Date.now(),
title: input,
};
await addItem(newTask);
setInput("");
loadTasks();
};
return (
<div>
<h2>IndexedDB Task List</h2>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={handleAdd}>Add</button>
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
</div>
);
}
What Is Happening Internally
When component mounts:
useEffectrunsgetAllItems()opens DB- Starts readonly transaction
- Gets all records
- Updates React state
When adding:
addItem()opens DB- Starts readwrite transaction
- Writes item
- React reloads data
Avoid Reopening DB Every Time
Right now openDB() runs on every call.
Better pattern:
- Open DB once
- Cache reference
- Reuse it
Cleaner Pattern: Using a Singleton DB Instance
let dbInstance: IDBDatabase | null = null;
export async function getDB() {
if (dbInstance) return dbInstance;
dbInstance = await openDB();
return dbInstance;
}
Now use getDB() inside CRUD functions.
This avoids repeated open calls.
Even Better — Use idb Library
Raw IndexedDB is verbose and event-based.
In real production, most teams use:
idb
It wraps IndexedDB in Promise syntax.
Example:
import { openDB } from "idb";
const db = await openDB("MyAppDB", 1, {
upgrade(db) {
db.createObjectStore("items", { keyPath: "id" });
},
});
await db.put("items", { id: 1, title: "Hello" });
const all = await db.getAll("items");
When Do We Use IndexedDB in React Apps?
Use it when:
- Building offline-first apps
- Caching API responses
- Storing large datasets
- Storing files/blobs
- PWA implementations
Not for:
Small temporary state
Simple UI preferences (use localStorage)