IndexedDB : avec idb et Dexie
L’API native d’IndexedDB est verbeuse. Deux bibliothèques répandues la simplifient : idb (fine couche de promesses) et Dexie (surcouche complète avec requêtes).
| Native + utilitaires | idb |
Dexie |
|
|---|---|---|---|
| Poids | 0 | Très léger (~1 Ko) | Plus lourd (~25 Ko) |
| Style | Événements | Promesses, proche de l’API native | Requêtes en chaîne (where().equals()) |
| Requêtes | Index et curseurs | Index et curseurs | Requêtes riches, filtres combinés |
| Courbe d’apprentissage | Raide | Faible (si l’API native est connue) | Faible |
| Observation des changements | À écrire | À écrire | liveQuery |
| Bon choix pour | Très petit besoin | Garder le contrôle | Applications avec beaucoup de données |
npm install idbimport { openDB } from 'idb';
const db = await openDB('boutique', 2, { upgrade(db, ancienne, nouvelle, transaction) { if (ancienne < 1) { const clients = db.createObjectStore('clients', { keyPath: 'id', autoIncrement: true, }); clients.createIndex('email', 'email', { unique: true }); } if (ancienne < 2) { transaction.objectStore('clients').createIndex('nom', 'nom'); } }, blocked() { console.warn('Mise à jour bloquée par un autre onglet'); }, blocking() { db.close(); }, // un autre onglet veut mettre à jour});
await db.add('clients', { nom: 'Alice', email: 'alice@exemple.com' });await db.put('clients', { id: 1, nom: 'Alice Martin', email: 'alice@exemple.com',});const client = await db.get('clients', 1);const tous = await db.getAll('clients');const parEmail = await db.getFromIndex('clients', 'email', 'alice@exemple.com');const quelquesUns = await db.getAllFromIndex( 'clients', 'nom', IDBKeyRange.bound('A', 'M'),);await db.delete('clients', 1);Transaction avec plusieurs opérations :
const tx = db.transaction(['clients', 'commandes'], 'readwrite');await Promise.all([ tx.objectStore('commandes').add({ numero: 'C-1', clientId: 1 }), tx.objectStore('clients').put({ id: 1, nom: 'Alice', nbCommandes: 1 }), tx.done, // promesse : résolue quand la transaction est validée]);Curseurs avec for await :
const tx = db.transaction('clients');for await (const curseur of tx.store.index('nom')) { console.log(curseur.value.nom);}await tx.done;Avec TypeScript
Section intitulée « Avec TypeScript »import { openDB, type DBSchema } from 'idb';
interface Boutique extends DBSchema { clients: { key: number; value: { id?: number; nom: string; email: string }; indexes: { email: string; nom: string }; };}
const db = await openDB<Boutique>('boutique', 1, { upgrade(db) { const s = db.createObjectStore('clients', { keyPath: 'id', autoIncrement: true, }); s.createIndex('email', 'email', { unique: true }); s.createIndex('nom', 'nom'); },});
const client = await db.get('clients', 1); // type : { id?: number; nom: string; email: string } | undefinednpm install dexieimport Dexie from 'dexie';
export const db = new Dexie('boutique');
db.version(1).stores({ clients: '++id, &email, nom', // ++id : clé auto-incrémentée ; &email : index unique ; nom : index commandes: 'numero, clientId, date, [statut+date]', // index composé});
await db.clients.add({ nom: 'Alice', email: 'alice@exemple.com' });await db.clients.put({ id: 1, nom: 'Alice Martin', email: 'alice@exemple.com',});const client = await db.clients.get(1);await db.clients.delete(1);| Syntaxe du schéma | Sens |
|---|---|
++id |
Clé primaire auto-incrémentée |
id |
Clé primaire fournie |
&email |
Index unique |
*etiquettes |
Index multi-entrées (champ tableau) |
[a+b] |
Index composé |
nom |
Index simple |
Seuls les champs indexés sont listés ; les autres champs sont stockés sans déclaration.
Requêtes
Section intitulée « Requêtes »const adultes = await db.clients .where('age') .between(18, 65, true, true) .toArray();const alice = await db.clients .where('email') .equals('alice@exemple.com') .first();const debutNom = await db.clients .where('nom') .startsWithIgnoreCase('al') .toArray();const recents = await db.commandes .orderBy('date') .reverse() .limit(10) .toArray();const payeesJanvier = await db.commandes .where('[statut+date]') .between(['payee', '2026-01-01'], ['payee', '2026-01-31'], true, true) .toArray();const filtres = await db.clients .filter((c) => c.nom.length > 5 && c.actif) .toArray();const nb = await db.clients.where('nom').equals('Alice').count();await db.clients.where('actif').equals(0).delete();await db.clients.where('nom').equals('Alice').modify({ actif: true });filter() parcourt les enregistrements en JavaScript : pratique mais sans index.
Transactions
Section intitulée « Transactions »await db.transaction('rw', db.clients, db.commandes, async () => { const id = await db.clients.add({ nom: 'Bob', email: 'bob@exemple.com' }); await db.commandes.add({ numero: 'C-2', clientId: id, date: '2026-02-01', statut: 'nouvelle', });}); // tout est enregistré, ou rienDans une transaction Dexie, on peut utiliser await sur les opérations Dexie sans fermer la transaction (Dexie gère ce point délicat) ; attendre une autre promesse (fetch) la ferme toujours.
Migrations
Section intitulée « Migrations »db.version(1).stores({ clients: '++id, &email' });
db.version(2) .stores({ clients: '++id, &email, nom' }) .upgrade(async (tx) => { await tx .table('clients') .toCollection() .modify((c) => { c.nom ??= c.email.split('@')[0]; }); });Conserver les déclarations des versions précédentes tant que des utilisateurs peuvent en venir.
Observer les changements : liveQuery
Section intitulée « Observer les changements : liveQuery »import { liveQuery } from 'dexie';
const observable = liveQuery(() => db.clients.where('actif').equals(1).toArray(),);
const abonnement = observable.subscribe({ next: (clients) => console.log('liste mise à jour', clients), error: (e) => console.error(e),});
// plus tardabonnement.unsubscribe();liveQuery ré-exécute la requête à chaque modification des données concernées (y compris depuis un autre onglet). Avec React, le paquet dexie-react-hooks fournit useLiveQuery :
import { useLiveQuery } from 'dexie-react-hooks';
function Clients() { const clients = useLiveQuery(() => db.clients.orderBy('nom').toArray(), []); if (!clients) return <p>Chargement…</p>; return ( <ul> {clients.map((c) => ( <li key={c.id}>{c.nom}</li> ))} </ul> );}TypeScript avec Dexie
Section intitulée « TypeScript avec Dexie »import Dexie, { type EntityTable } from 'dexie';
interface Client { id: number; nom: string; email: string; actif: boolean;}
const db = new Dexie('boutique') as Dexie & { clients: EntityTable<Client, 'id'>;};db.version(1).stores({ clients: '++id, &email, nom, actif' });
const alice: Client | undefined = await db.clients.get(1);Dans un framework avec rendu serveur
Section intitulée « Dans un framework avec rendu serveur »Comme pour localStorage, indexedDB n’existe pas côté serveur : n’y accéder que dans le navigateur (effet, événement, composant client, script client). Instancier la base paresseusement et ne pas l’importer dans du code exécuté pendant le rendu serveur (voir Avec Astro, React et Next.js).
Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
indexedDB is not defined |
Import ou exécution côté serveur : déplacer dans le navigateur |
Dexie: DatabaseClosedError |
Base fermée (suppression, mise à jour dans un autre onglet) : rouvrir |
Dexie: SchemaError |
Syntaxe de schéma invalide ; index absent de stores() |
Une requête where('champ') échoue |
champ n’est pas un index déclaré |
ConstraintError à add |
Clé ou index unique déjà utilisé : put ou traitement de l’erreur |
| La transaction se ferme | await d’une opération étrangère à Dexie |
idb : tx.done jamais résolue |
Transaction jamais validée : opérations en erreur ou non attendues |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Aucune : bibliothèques JavaScript exécutées dans le navigateur (installation avec npm, voir Node.js : npm).
Pour aller plus loin
Section intitulée « Pour aller plus loin »- idb, Dexie
- Suite : Hors ligne et synchronisation