Skip to content

Demo: Todo List ​

A todo app where the database is the only state. The list and the counters are two independent live queries; add, tick, rename or delete an item (here or in another tab) and both update on their own.

How it works ​

1. A live query for the list ​

useLiveQuery runs the query, subscribes to every table it reads, and re-runs it when one changes. Passing [filter, search] also re-runs it when those refs change:

ts
const { data: todos } = useLiveQuery(
  () => db.from('todos')
        .where('done', '=', 0)                 // when filter === 'active'
        .whereContains('title', search.value)
        .orderBy('id', 'desc'),
  [filter, search],
  { db },
);

2. A second live query for the counters ​

ts
const { data: stats } = useLiveQuery(
  () => db.from('todos').groupBy('done').count('id', 'n'),
  { db },
);
// -> [{ done: 0, n: 3 }, { done: 1, n: 1 }]

3. Plain writes, no manual refresh ​

ts
await db.insert('todos', { title, done: 0, created_at: new Date().toISOString() });
await db.update('todos', id, { done: 1 });
await db.delete('todos', id);
await db.from('todos').where('done', '=', 1).delete();   // "Clear completed"

Nothing re-fetches after a write. The live queries above see the change and update the UI.

4. Schema and storage ​

ts
const db = await WebDB.open({ name: 'demo_todo', storage: 'idb' });

await db.createTable('todos', [
  { name: 'id', type: 'INT32', primaryKey: true, autoInc: true },
  { name: 'title', type: 'TEXT', notNull: true },
  { name: 'done', type: 'INT32', notNull: true },   // 0 | 1
  { name: 'created_at', type: 'TEXT', notNull: true },
]);

Source code ​

FileWhat it does
todo/TodoApp.vueThe Vue component with the live queries
todo/db.tsSchema, seed data, queries and writes
DemoFrame.vueOpens the IndexedDB database and renders the app

👉 Browse the full source on GitHub

Next: Kanban board · Calendar · Typo-Tolerant Search with UDFs

Released under the MIT License.