Aller au contenu

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
Fenêtre de terminal
npm install idb
import { 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;
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 } | undefined
Fenêtre de terminal
npm install dexie
import 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.

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.

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 rien

Dans 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.

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.

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 tard
abonnement.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>
);
}
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);

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).

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).