Home | Send Feedback

Dexie.js live queries

Published: 22. January 2022  •  javascript, database, ionic

Every web browser has a built-in database called IndexedDB. IndexedDB is a database that enables a JavaScript application to store a significant amount of structured data inside the browser, including files/blobs.

Most IndexedDB API functions are asynchronous, but the API was added to the browsers before introducing Promise, and async/await. Therefore, the API uses callbacks which makes development a bit complicated. Fortunately, there are free libraries available that wrap the IndexedDB API and make it more programmer-friendly by providing a Promise-based API.

One of my go-to wrapper library is Dexie.js. It is a thin layer around IndexedDB, provides a Promise-based API and adds many convenient functionalities. See my previous blog posts about IndexedDB and Dexie.js if you want to learn more.

The author of Dexie.js recently (November 2021) released a new version (3.2) of Dexie.js with a very interesting feature that I will show with an example in this blog post.

The feature is called live query and enables an application to subscribe to IndexedDB queries. It allows for better integration with frontend libraries.

I will show you the feature with an Angular/Ionic application, but Dexie.js also supports this feature in React, Vue, Svelte and in plain JavaScript.

Example

The demo application is a trivial todo application with two pages. The list page displays all stored todo entries, and the edit page allows the user to enter and update todos.


The todo entries are stored in an IndexedDB database with the name tododb and object store todos.

todo database


The application runs the following code to set up the database and define the schema of the todo object store.

import Dexie from 'dexie';

export class TodoDb extends Dexie {
  todos!: Dexie.Table<Todo, string>;

  constructor() {
    super('tododb');
    this.version(1).stores({
      todos: 'id'
    });
  }
}

export interface Todo {
  id: string;
  priority: 'high' | 'normal' | 'low';
  dueDate?: string;
  description: string;
}



todo-db.ts


Live Query

In the TypeScript code of the list component, we see how to create a live query. this.todoService.allTodos() is a simple IndexedDB query that returns all records from the object store.

  allTodos(): Promise<Todo[]> {
    return this.db.todos.toArray();
  }

todo.service.ts

To convert this query into a live query, you have to wrap it with the liveQuery function (imported from 'dexie'). This function returns an Observable (also imported from dexie), which is compatible with the RxJs Observable.

import {liveQuery, Observable} from 'dexie';

list.page.ts

export class ListPage {
  public readonly todos$: Observable<Todo[]>;

  constructor(private readonly todoService: TodoService,
              private readonly messagesService: MessagesService) {
    this.todos$ = liveQuery(() => this.todoService.allTodos());
  }

list.page.ts

The Observable is then used in the template with the async pipe to display all entries.

  <ion-card *ngFor="let todo of todos$ | async; trackBy: todoTrackBy" class="ion-margin">

list.page.html


Update

The user enters and updates todos on the edit page. When he clicks the Save button, the application stores the entered data in the database with the following code

  async save(todoForm: NgForm) {
    this.selectedTodo.dueDate = this.dueDate;
    this.selectedTodo.description = todoForm.value.description;
    this.selectedTodo.priority = todoForm.value.priority;

    await this.todoService.updateTodo(this.selectedTodo);
    this.messagesService.showSuccessToast('Todo successfully saved', 500);
    this.router.navigate(['/todo']);
  }

edit.page.ts

The updateTodo function from the service class uses put to either insert or update the object. If an object with the same primary key already exists, put will replace the given object; otherwise, it adds the object.

  updateTodo(todo: Todo): Promise<string> {
    return this.db.todos.put(todo);
  }

todo.service.ts

This update will automatically trigger the live query on the list component. Notice here that we execute a regular database operation. There is no special handling needed if you use live queries.


Delete

On the list page, I added a delete button that removes the entry from the database.

  async deleteTodo(id: string): Promise<void> {
    await this.todoService.deleteTodo(id);
    await this.messagesService.showSuccessToast('Todo successfully deleted', 500);
  }

list.page.ts

  deleteTodo(id: string): Promise<void> {
    return this.db.todos.delete(id);
  }

todo.service.ts

Like the update functionality, this code also interacts with the database. This database change will also trigger the live query and update the view accordingly.


This concludes this short overview of the new live query feature of Dexie.js. You have seen how this feature is very convenient to synchronize data stored in IndexeDB with other components of an application.