Which IndexedDB Library Should I Use: Dexie vs idb vs RxDB?
I tried using the native IndexedDB API directly. After writing 50 lines of boilerplate just to open a database and add a record, I gave up. The API is verbose, callback-heavy, and easy to get wrong.
A Reddit thread confirmed what I suspected: “The IndexedDB API is awful, but there’s plenty of popular libraries that make it easy.”
The top recommendations were consistent: “idb, dexie, rxdb.” But which one should I use? I spent a day testing all three to find out.
The Native IndexedDB Problem
Here’s what I was dealing with:
// Opening a database with native IndexedDBconst request = indexedDB.open('MyDatabase', 1);
request.onupgradeneeded = (event) => { const db = event.target.result; const store = db.createObjectStore('users', { keyPath: 'id' }); store.createIndex('email', 'email', { unique: true });};
request.onsuccess = (event) => { const db = event.target.result;
// Adding a record const transaction = db.transaction(['users'], 'readwrite'); const store = transaction.objectStore('users');
addRequest.onsuccess = () => { console.log('Record added');
// Now query it const getRequest = store.get(1); getRequest.onsuccess = (e) => { console.log(e.target.result); }; getRequest.onerror = () => { console.error('Failed to get record'); }; };
addRequest.onerror = () => { console.error('Failed to add record'); };};
request.onerror = () => { console.error('Failed to open database');};That’s 35 lines for open, add, and get. And I haven’t even handled errors properly.
The Three Contenders
1. Dexie.js - The Balanced Choice
Dexie wraps IndexedDB with a clean Promise-based API and adds powerful query capabilities.
import Dexie from 'dexie';
const db = new Dexie('MyDatabase');db.version(1).stores({ users: '++id, name, email'});
// Add a user
// Get by IDconst user = await db.users.get(1);
// Query by index
// Complex queriesconst adults = await db.users .where('age') .between(18, 65) .and(user => user.active === true) .toArray();The same operations in 15 lines. Much better.
What I liked:
- Clean, intuitive API
- Powerful WhereClause for complex queries
- React hooks integration with
useLiveQuery - Optional Dexie Cloud for real-time sync
- Good TypeScript support
What I didn’t:
- Bundle size is ~50KB minified
- Adds its own query language on top of IndexedDB
- Learning curve for advanced features
2. idb - The Minimalist’s Choice
idb stays close to native IndexedDB but makes it promise-based. It’s tiny.
import { openDB } from 'idb';
const db = await openDB('MyDatabase', 1, { upgrade(db) { const store = db.createObjectStore('users', { keyPath: 'id' }); store.createIndex('email', 'email', { unique: true }); }});
// Add a user
// Get by IDconst user = await db.get('users', 1);
// Get by index
// Query allconst allUsers = await db.getAll('users');What I liked:
- Only ~3KB minified
- API mirrors native IndexedDB closely
- Easy to migrate existing IndexedDB code
- Tree-shakeable
- No query language to learn
What I didn’t:
- Still need to understand IndexedDB concepts
- No advanced query helpers
- You write more code for complex operations
3. RxDB - The Reactive Powerhouse
RxDB is a full-featured reactive database built on top of IndexedDB.
import { createRxDatabase, addRxPlugin } from 'rxdb';import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';
addRxPlugin(require('pouchdb-adapter-idb'));
const db = await createRxDatabase({ name: 'mydatabase', storage: getRxStorageDexie()});
await db.addCollections({ users: { schema: { version: 0, primaryKey: 'id', type: 'object', properties: { id: { type: 'string', maxLength: 36 }, name: { type: 'string' }, email: { type: 'string' } } } }});
// Insert
// Reactive query - auto-updates!db.users.find().$.subscribe(users => { console.log('Users changed:', users);});
// Replication with backendawait db.users.syncCouchDB({ remote: 'http://localhost:5984/mydb'});What I liked:
- Reactive data streams with RxJS
- Built-in replication (HTTP, GraphQL, CouchDB)
- Schema validation
- Works on Node.js too
- Offline-first by design
What I didn’t:
- Bundle size is ~200KB
- Requires learning RxJS patterns
- Schema definition is verbose
- More complexity than I needed for simple projects
Decision Framework
After testing, I built a decision flow:
1. Do I need real-time sync or offline-first architecture? YES -> RxDB NO -> Go to step 2
2. Do I need complex queries (range, compound, full-text)? YES -> Dexie NO -> Go to step 3
3. Am I migrating existing IndexedDB code? YES -> idb NO -> Go to step 4
4. Is bundle size critical (<5KB)? YES -> idb NO -> Dexie (easiest to learn)Feature Comparison
Feature | Dexie | idb | RxDB---------------------------|-----------|-----------|----------Bundle size | ~50KB | ~3KB | ~200KBPromise-based API | Yes | Yes | YesComplex queries | Yes | Manual | YesReactive subscriptions | Via hooks | No | Yes (RxJS)Built-in replication | Dexie Cloud| No | YesSchema validation | No | No | YesLearning curve | Low | Low | HighTypeScript support | Excellent | Good | GoodTree-shakeable | Partial | Yes | PartialReal-World Scenarios
Scenario 1: Simple Todo App
I’m building a todo app that stores tasks offline. No sync, no complex queries.
import { openDB } from 'idb';
const db = await openDB('todo-app', 1, { upgrade(db) { db.createObjectStore('todos', { keyPath: 'id' }); }});
// CRUD operationsawait db.put('todos', { id: Date.now(), text: 'Buy milk', done: false });const todos = await db.getAll('todos');await db.delete('todos', id);Verdict: idb - 3KB, simple API, perfect for CRUD apps.
Scenario 2: Offline-First Note Taking App
Notes need to sync across devices, support full-text search, and work offline.
import { createRxDatabase } from 'rxdb';import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';
const db = await createRxDatabase({ name: 'notes', storage: getRxStorageDexie()});
await db.addCollections({ notes: { schema: { version: 0, primaryKey: 'id', type: 'object', properties: { id: { type: 'string', maxLength: 36 }, title: { type: 'string' }, content: { type: 'string' }, updatedAt: { type: 'number' } }, required: ['id', 'title', 'content'] } }});
// Reactive: UI updates automaticallydb.notes.find().sort({ updatedAt: 'desc' }).$.subscribe(notes => { renderNotes(notes);});
// Sync with backendconst replicationState = db.notes.syncGraphQL({ url: 'http://api.example.com/graphql', headers: { Authorization: 'Bearer token' }});Verdict: RxDB - Built for offline-first with sync.
Scenario 3: E-commerce Product Catalog
Need to cache products, search by category, filter by price range, sort by rating.
import Dexie from 'dexie';
const db = new Dexie('ProductCatalog');db.version(1).stores({ products: '++id, category, price, rating, name'});
// Complex queryconst results = await db.products .where('category').equals('electronics') .and(product => product.price >= 100 && product.price <= 500) .sortBy('rating');
// Full-text search (with Dexie's whereClause)const searchTerm = 'laptop';const searchResults = await db.products .filter(product => product.name.toLowerCase().includes(searchTerm)) .toArray();
// Bulk insert cached productsawait db.products.bulkPut(productsFromAPI);Verdict: Dexie - Query capabilities perfect for filtering/searching.
Bundle Size Reality Check
I ran a quick test with all three:
Library | Minified | Gzipped | Impact on initial load----------|----------|---------|------------------------idb | 3.2KB | 1.1KB | NegligibleDexie | 48KB | 14KB | Noticeable but acceptableRxDB | 210KB | 62KB | Significant, code-split recommendedFor RxDB, I’d recommend dynamic imports:
// Don't import at top level// import { createRxDatabase } from 'rxdb'; // BAD
// Load on demandasync function initDatabase() { const { createRxDatabase } = await import('rxdb'); const { getRxStorageDexie } = await import('rxdb/plugins/storage-dexie');
return createRxDatabase({ name: 'app', storage: getRxStorageDexie() });}React Integration
All three work with React, but differently:
// Dexie: Built-in hooksimport { useLiveQuery } from 'dexie-react-hooks';
function UserList() { const users = useLiveQuery(() => db.users.toArray()); return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;}
// idb: Use SWR or React Queryimport useSWR from 'swr';
function UserList() { const { data: users } = useSWR('users', () => db.getAll('users')); return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;}
// RxDB: Native RxJS integrationimport { useEffect, useState } from 'react';
function UserList() { const [users, setUsers] = useState([]);
useEffect(() => { const sub = db.users.find().$.subscribe(setUsers); return () => sub.unsubscribe(); }, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;}The Verdict
For most web applications, start with Dexie. Its query capabilities and React integration provide immediate value. The ~50KB bundle is worth the developer experience improvement.
Choose idb if:
- You’re comfortable with IndexedDB concepts
- Bundle size is critical
- You’re migrating existing IndexedDB code
- You only need simple CRUD operations
Choose RxDB if:
- You’re building an offline-first application
- You need real-time data synchronization
- You want reactive data streams
- You’re okay with the learning curve and bundle size
The Reddit thread was right - all three solve the “awful IndexedDB API” problem. The key is matching your project’s needs to the library’s strengths.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Dexie.js Documentation
- 👨💻 idb on GitHub
- 👨💻 RxDB Documentation
- 👨💻 Reddit: Best IndexedDB wrapper libraries
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments