Skip to content

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:

native-indexeddb-pain.js
// Opening a database with native IndexedDB
const 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');
const addRequest = store.add({ id: 1, name: 'Alice', email: '[email protected]' });
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.

dexie-basic.js
import Dexie from 'dexie';
const db = new Dexie('MyDatabase');
db.version(1).stores({
users: '++id, name, email'
});
// Add a user
await db.users.add({ name: 'Alice', email: '[email protected]' });
// Get by ID
const user = await db.users.get(1);
// Query by index
const found = await db.users.where('email').equals('[email protected]').first();
// Complex queries
const 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.

idb-basic.js
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
await db.add('users', { id: 1, name: 'Alice', email: '[email protected]' });
// Get by ID
const user = await db.get('users', 1);
// Get by index
const found = await db.getFromIndex('users', 'email', '[email protected]');
// Query all
const 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.

rxdb-basic.js
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
await db.users.insert({ id: '1', name: 'Alice', email: '[email protected]' });
// Reactive query - auto-updates!
db.users.find().$.subscribe(users => {
console.log('Users changed:', users);
});
// Replication with backend
await 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:

Which library should I use?
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 comparison table
Feature | Dexie | idb | RxDB
---------------------------|-----------|-----------|----------
Bundle size | ~50KB | ~3KB | ~200KB
Promise-based API | Yes | Yes | Yes
Complex queries | Yes | Manual | Yes
Reactive subscriptions | Via hooks | No | Yes (RxJS)
Built-in replication | Dexie Cloud| No | Yes
Schema validation | No | No | Yes
Learning curve | Low | Low | High
TypeScript support | Excellent | Good | Good
Tree-shakeable | Partial | Yes | Partial

Real-World Scenarios

Scenario 1: Simple Todo App

I’m building a todo app that stores tasks offline. No sync, no complex queries.

todo-app-with-idb.js
import { openDB } from 'idb';
const db = await openDB('todo-app', 1, {
upgrade(db) {
db.createObjectStore('todos', { keyPath: 'id' });
}
});
// CRUD operations
await 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.

notes-app-with-rxdb.js
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 automatically
db.notes.find().sort({ updatedAt: 'desc' }).$.subscribe(notes => {
renderNotes(notes);
});
// Sync with backend
const 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.

catalog-with-dexie.js
import Dexie from 'dexie';
const db = new Dexie('ProductCatalog');
db.version(1).stores({
products: '++id, category, price, rating, name'
});
// Complex query
const 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 products
await db.products.bulkPut(productsFromAPI);

Verdict: Dexie - Query capabilities perfect for filtering/searching.

Bundle Size Reality Check

I ran a quick test with all three:

Bundle sizes after tree-shaking (Vite production build)
Library | Minified | Gzipped | Impact on initial load
----------|----------|---------|------------------------
idb | 3.2KB | 1.1KB | Negligible
Dexie | 48KB | 14KB | Noticeable but acceptable
RxDB | 210KB | 62KB | Significant, code-split recommended

For RxDB, I’d recommend dynamic imports:

lazy-load-rxdb.js
// Don't import at top level
// import { createRxDatabase } from 'rxdb'; // BAD
// Load on demand
async 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:

react-integrations.jsx
// Dexie: Built-in hooks
import { 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 Query
import 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 integration
import { 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments