Dexie.js - Documentation

Getting Started

Installation

Dexie.js is an easy-to-install JavaScript library. The most common way is via npm or yarn:

npm install dexie
# or
yarn add dexie

Alternatively, you can include it directly from a CDN, though this is less ideal for larger projects due to version management challenges:

<script src="https://unpkg.com/dexie"></script> </html>

Remember to check the latest version number on the Dexie.js website or npm registry and update the URL or package version accordingly.

Basic Usage

Dexie.js provides an intuitive, Promise-based API for interacting with IndexedDB. At its core, you define a database schema, open the database, and then use methods to perform CRUD (Create, Read, Update, Delete) operations.

Your First Database

Let’s create a simple database to store a list of names.

// Import Dexie
import Dexie from 'dexie';

// Define the database schema
const db = new Dexie('myDatabase');
db.version(1).stores({
  names: '++id, name' // '++id' creates an auto-incrementing primary key
});

// Add data
db.names.add({ name: 'John Doe' }).then(() => {
  console.log('Data added successfully!');
});

// Retrieve data
db.names.toArray().then((names) => {
  console.log('Retrieved names:', names);
});

This code snippet first defines a database named ‘myDatabase’ with a single table named ‘names’. The ++id in the stores definition creates an auto-incrementing primary key for each record. We then add a name and retrieve all names from the database using promises. Remember to handle potential errors using .catch() if needed.

Opening a Database

Dexie.js handles database opening asynchronously. The constructor (new Dexie('myDatabase')) doesn’t immediately open the database; it creates a Dexie object that represents the database. The database is only opened when you perform an operation that requires accessing the database (e.g., adding, getting, or updating data). This happens implicitly when you invoke methods like db.names.add(), or explicitly when you call db.open(). While db.open() is rarely needed, it can be useful for pre-emptive opening in specific scenarios like before the main application logic starts, or to explicitly handle database opening errors before other actions are taken. For example:

import Dexie from 'dexie';

const db = new Dexie('myDatabase');
db.version(1).stores({
  names: '++id, name'
});

db.open().then(() => {
    console.log("Database opened successfully!");
    // Continue with other operations here.
}).catch(error => {
    console.error("Error opening database:", error);
    // Handle database opening errors appropriately, perhaps by displaying an error message to the user.
});

Error handling via .catch() is crucial for robust application behaviour. Always handle potential errors when interacting with the database.

Defining Tables

Table Schema

Dexie.js tables are defined within the stores object of a database version. The stores object maps table names to their schema definitions. The schema specifies the table’s columns and their properties, primarily the primary key. It uses a string-based syntax where column names are separated by commas.

A simple schema might look like this:

db.version(1).stores({
  users: 'id, name, email'
});

This defines a table named users with three columns: id, name, and email. Note that no data types are explicitly specified; Dexie.js handles data type inference.

Defining Primary Keys

A primary key uniquely identifies each record in a table. Dexie.js supports auto-incrementing primary keys and custom primary keys.

Auto-Incrementing Primary Keys:

Use ++id to create an auto-incrementing integer primary key named id. This is often the simplest and most convenient approach.

db.version(1).stores({
  products: '++id, name, price'
});

Custom Primary Keys:

For custom primary keys, simply list the column name(s) that will serve as the primary key. Dexie.js will use these columns to ensure uniqueness. You can have a composite primary key consisting of multiple columns.

db.version(1).stores({
  items: 'orderId, itemId, description' // orderId and itemId together form the primary key
});

Indexes

Indexes speed up queries on specific columns. Indexes are defined by adding + before a column name in the stores definition. Multiple indexes can be created per table.

db.version(1).stores({
  products: '++id, name, +price, +category'
});

This creates indexes on the price and category columns, enabling faster searches based on these fields. Note that the primary key is always indexed implicitly.

Defining Collections

While Dexie.js doesn’t explicitly use the term “collections” in the same way as some other databases, the concept is embodied by its tables. Each table in Dexie.js acts as a collection of objects. The stores object definition in the db.version() method is what defines the structure and schema for these collections/tables. Therefore, defining a table automatically defines a collection. All interactions with the table—adding, retrieving, updating, deleting—are effectively operations on the collection represented by that table.

Data Manipulation

Adding Data (add, put, bulkAdd, bulkPut)

Retrieving Data (get, getAll, toCollection)

Updating Data (update, put)

Deleting Data (delete, clear)

Transactions

Dexie.js supports transactions to ensure data integrity. Use db.transaction() to wrap multiple database operations within a single transaction. If any operation fails within the transaction, the entire transaction is rolled back.

db.transaction('rw', db.users, db.products, function* () {
  yield db.users.add({ name: 'Jane Doe' });
  yield db.products.add({ name: 'Widget', price: 10 });
});

Where Clauses

Dexie.js uses .where() to filter results based on various conditions:

db.users.where('age').above(25).toArray(); // Get all users older than 25
db.products.where('price').between(10, 20).toArray(); //Get products with price between 10 and 20
db.users.where('name').startsWith('J').toArray(); // Get all users whose name starts with 'J'

You can chain multiple .where() clauses, though this is more efficient with indexes on the relevant fields.

Ordering Results

Use .orderBy() to sort the results of a query:

db.products.orderBy('price').toArray(); // Order products by price (ascending)
db.products.orderBy('price').reverse().toArray(); // Order products by price (descending)
db.products.orderBy('category', 'price').toArray();// Order by category, then price.

Multiple columns can be used for multi-level sorting.

Filtering Results

The .filter() method allows you to filter the results based on a custom function:

db.products.where('price').above(10).filter(product => product.category === 'Electronics').toArray();

Limiting Results

Use .limit() to restrict the number of results returned:

db.products.orderBy('price').limit(5).toArray(); // Get the 5 cheapest products

.offset() can be used in conjunction with .limit() to skip a certain number of results:

db.products.orderBy('price').offset(5).limit(5).toArray();// Get products ranked 6-10 by price

Remember to chain these methods appropriately for complex queries. Using indexes whenever possible significantly improves query performance.

Advanced Features

Database Events

Dexie.js emits several events that allow you to react to database changes and lifecycle events. These events are typically handled using the .on() method. Common events include:

Example:

db.on('ready', () => console.log('Database is ready'));
db.on('blocked', () => console.warn('Database is blocked'));

Collections

Dexie Collections, returned by db.table.toCollection(), offer a fluent API for building complex queries and chaining operations before executing them with methods like toArray(), each(), count(), etc. They act as a sort of deferred execution mechanism, giving more control over how and when queries are executed. Efficient for complex queries or scenarios requiring multiple operations on the same subset of data.

Upgrade Strategies

When upgrading database versions, you must handle schema changes within the upgrade event handler of the Dexie.version() method. This ensures data integrity during schema migrations. Use transactions within the upgrade function to ensure atomicity of operations.

Hooks

Dexie.js allows defining hooks to run specific code before or after database operations. These hooks provide opportunities for logging, validation, or other custom logic:

Hooks are defined using the hook() method. For instance, creating a before hook that logs operations:

db.users.hook('beforeAdd', (primKey, obj, transaction) => {
    console.log("Adding user:", obj);
});

Custom Table Methods

You can add custom methods to your tables to encapsulate frequently used operations or create reusable logic. This helps to enhance code readability and maintainability:

db.users.method('findByEmail', function(email) {
  return this.where('email').equals(email).first();
});

// Usage:
db.users.findByEmail('test@example.com').then(user => { /* ... */ });

Versioning and Migrations

Dexie.js uses versions to manage schema changes. Each version is defined using db.version(number), and the upgrade function within this definition contains the necessary upgrade logic. For example:

db.version(2).stores({
    users: '++id, name, email, address'
}).upgrade(function (trans) {
    if (trans.db.version === 1) {
        // upgrade logic from version 1 to version 2
        trans.db.users.toCollection().modify({address: null}); //add a new field `address`
    }
});

Working with Blobs and Files

Dexie.js handles Blobs and Files seamlessly. You can store and retrieve them just like any other data type, using methods like add() and get(). The key is that the database will store references to the blobs, not the entire blob data directly in the table itself.

Error Handling

Always use .catch() to handle potential errors during database operations. Dexie.js throws errors for various reasons, including invalid queries, database failures, and schema mismatches. Appropriate error handling is crucial for application robustness. Check for specific error types (e.g., Dexie.NoSuchPrimaryKey, Dexie.SchemaError, etc.) to provide user-friendly error messages.

Performance Tuning

Working with Different Data Types

Dexie.js handles various JavaScript data types with minimal friction. However, understanding how these types are stored and retrieved can lead to more efficient and robust code.

Numbers

Numbers are stored as their native JavaScript numeric representations (both integers and floating-point numbers). Dexie.js automatically handles type inference and conversion. No special handling is usually needed.

db.myTable.add({ count: 10, price: 99.99 });

Strings

Strings are stored as native JavaScript strings. No special encoding or escaping is typically required, though you might need to handle encoding/decoding if you’re interacting with external systems that might use different character encodings.

db.myTable.add({ name: 'John Doe', description: 'A long string with various characters.' });

Dates

Dates are stored as timestamps (the number of milliseconds since the Unix epoch). When retrieving a Date object from the database, Dexie.js automatically converts the timestamp back into a Date object.

const now = new Date();
db.myTable.add({ created: now });

// ... later ...

db.myTable.get(1).then(item => {
  console.log(item.created instanceof Date); // true
});

Arrays

Arrays are stored as JavaScript arrays. There are no special constraints on the data types within the array; they can be a mix of numbers, strings, objects, etc. However, for better queryability, consider structuring your data to avoid deeply nested arrays if possible.

db.myTable.add({ tags: ['javascript', 'indexeddb', 'dexie'] });

Objects

Objects are stored as JavaScript objects. They are generally handled without special treatment, but remember that you cannot directly query the properties of nested objects unless you design your schema to reflect the necessary structure for querying.

db.myTable.add({ user: { name: 'Jane', age: 30 } });

To efficiently query nested data, flatten your object structure where needed for indexing or pre-process the data for easier querying.

JSON

While Dexie.js doesn’t have a specific JSON type, you can store JSON data as strings. You will need to explicitly parse the JSON strings using JSON.parse() when retrieving and JSON.stringify() when storing.

const jsonData = { name: 'Test', data: [1, 2, 3] };
db.myTable.add({ data: JSON.stringify(jsonData) }).then(() => {
  // ...
});

// Retrieving and parsing:
db.myTable.get(1).then(item => {
    const parsedData = JSON.parse(item.data);
    // Use parsedData
});

Blobs

Blobs (Binary Large Objects) are stored as Blobs. Dexie.js handles them efficiently, storing references to the actual blob data within IndexedDB. When retrieving a Blob, you receive a new Blob object representing the data. For large Blobs, consider optimizing your data handling to avoid loading unnecessary data into memory.

const myBlob = new Blob(['Hello, world!'], { type: 'text/plain' });
db.myTable.add({ myBlob: myBlob });

Remember that efficient data modeling and indexing are critical for optimal performance when working with large datasets or complex data structures in Dexie.js. Consider optimizing your data structure to support efficient querying.

Best Practices

Database Design

Careful database design is crucial for performance and maintainability. Consider these points:

Schema Optimization

Query Optimization

Error Handling Best Practices

Testing and Debugging

API Reference

This section provides a concise overview of the core Dexie.js API objects. For complete and up-to-date details, refer to the official Dexie.js documentation.

Dexie Object

The Dexie object represents the entire database. It’s created using the new Dexie(dbName) constructor. Key methods include:

Table Object

The Table object represents a single table within the database. It’s obtained via db.table(tableName). Key methods include:

Collection Object

The Collection object represents a query that hasn’t been executed yet. It’s obtained via table.toCollection(). It allows for building complex queries by chaining multiple methods before finally executing the query with methods like:

Transaction Object

The Transaction object represents a database transaction. It’s created via db.transaction(). Key properties and methods:

KeyRange Object

The KeyRange object is used to specify a range of keys in where clauses. It’s not directly created by the user but is used by functions such as .where(...).between(a, b).

Helper Functions

Dexie.js provides several helper functions:

This API reference provides a high-level overview. Refer to the official documentation for detailed descriptions, examples, and exhaustive coverage of all options and methods. The official documentation is the most accurate and updated source for Dexie.js’s API.

Troubleshooting

This section covers common issues and strategies for resolving them when working with Dexie.js.

Common Errors

Debugging Techniques

Troubleshooting Browser Compatibility

Dexie.js generally supports modern browsers. However, ensure that you’re targeting browsers with adequate IndexedDB support. Older browsers might not support all Dexie.js features or might have different IndexedDB implementations that behave slightly differently.

Finding Help and Support

Remember to always provide a minimal, reproducible example when seeking help online. This makes it easier for others to understand your problem and assist you in finding a solution.