liveQuery()
Since 3.1.0-beta.1
Remarks
Turns a Promise-returning function that queries Dexie into an Observable.
Syntax
export function liveQuery<T>(
querier: () => T | Promise<T>
): Observable<T>;
Parameter | Description |
---|---|
querier | Function that returns a final result (Promise) |
Svelte and Angular
Svelte and Angular supports Observables natively so liveQuery() can be used directly.
The Svelte Store Contract implements a subset of the Ecmascript Observable specification draft which makes the return value of liveQuery() a fully valid Svelte Store by itself.
Angular supports Rx observables natively, and Rx Observables also are compliant with the Ecmascript Observable specification.
Svelte: Use the dollar prefix ($liveQueryReturnValue
)
Angular: Use the AsyncPipe (liveQueryReturnValue | async
).
React and Vue
For React apps, we provide a hook, useLiveQuery() that allows components to consume live queries.
For Vue, we still haven’t implemented any specific hook, but the observable returned from liveQuery() can be consumed using useObservable() from @vueuse/rxjs.
Examples
// db.js (DB declaration only - used in the following samples)
export const db = new Dexie('FriendDatabase');
db.version(1).stores({
friends: '++id, name, age'
});
Vanilla JS
import { liveQuery } from "dexie";
import { db } from './db';
const friendsObservable = liveQuery (
() => db.friends
.where('age')
.between(50, 75)
.toArray()
);
// Subscribe
const subscription = friendsObservable.subscribe({
next: result => console.log("Got result:", JSON.stringify(result)),
error: error => console.error(error)
});
// Unsubscribe
subscription.unsubscribe();
React
import { useLiveQuery } from "dexie-react-hooks";
import { db } from "./db";
export function FriendList () {
const friends = useLiveQuery(
() => db.friends.where("age").between(50, 75).toArray()
);
return <>
<h2>Friends</h2>
<ul>
{
friends?.map(friend =>
<li key={friend.id}>
{friend.name}, {friend.age}
</li>
)
}
</ul>
</>;
}
Svelte
<script>
import { liveQuery } from "dexie";
import { db } from "./db";
let friends = liveQuery(
() => db.friends.where("age").between(50, 75).toArray()
);
</script>
<div>
<h2>Friends</h2>
<ul>
{#each ($friends || []) as friend (friend.id)}
<li>{friend.name}, {friend.age}</li>
{/each}
</ul>
</div>
Other frameworks
On dexie.org we also show examples for Angular and Vue.
Deep Dive
The following vanilla JS example should explain how the observable works by looking at the print outs.
import { liveQuery } from "dexie";
import { db } from './db';
const friendsObservable = liveQuery (
() => db.friends
.where('age')
.between(50, 75)
.toArray()
);
const subscription = friendsObservable.subscribe({
next: result => console.log("Got result:", JSON.stringify(result)),
error: error => console.error(error)
});
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
await sleep(1000);
console.log("1. Adding friend");
const friendId = await db.friends.add({name: "Magdalena", age: 54});
await sleep(1000);
console.log("2. Changing age to 99");
await db.friends.update(friendId, {age: 99});
await sleep(1000);
console.log("3. Changing age to 55");
await db.friends.update(friendId, {age: 55});
await sleep(1000);
console.log("4. Setting property 'foo' to 'bar'");
await db.friends.update(friendId, {foo: "bar"});
await sleep(1000);
console.log("5. Deleting friend");
await db.friends.delete(friendId);
subscription.unsubscribe();
This sample is also available in Dexie.js/samples/liveQuery/liveQuery.html
The following output will be seen:
Got result: []
1. Adding friend
Got result: [{"name":"Magdalena","age":54,"id":1}]
2. Changing age to 99
Got result: []
3. Changing age to 55
Got result: [{"name":"Magdalena","age":55,"id":1}]
4. Setting property 'foo' to 'bar'
Got result: [{"name":"Magdalena","age":55,"id":1,"foo":"bar"}]
5. Deleting friend
Got result: []
Whenever a database change is made that would affect the result of your querier, your querier callback will be re-executed and your observable will emit the new result.
If you wonder how we can possibly detect whether a change would affect your querier, the details are:
- Any call to any Dexie API done during querier execution will be tracked
- The tracking is done using an efficient data structure for range collision detection, a range tree
- Every index being queried is tracked with the given range it queries. This makes it possible to detect whether an added object would fit within the range or not, also whether an update of an indexed property would make it become included or not.
- Whenever a write-transaction commits successfully, mutated parts (keys and ranges) are matched against active live queries using the range tree structure.
- Add-mutations: every indexed property is matched against active live queries
- Update-mutations: if an indexed property is updated, we detect whether it would become included into any live query where it was previously not included, or excluded from a query it was previously included in, or whether it updates properties on a result that was part of the query result.
- Delete-mutations: queries that have the same primary keys in their results will be triggered
- Whenever the querier is triggered, the subscribed ranges are cleared, the querier re-executed and the ranges or keys being queried this time will be tracked.
- Mutated rangesets are also broadcast across browsing contexts to wake up liveQueries in other tabs or workers
This is a simplified explanation of how the algorithm works. The raw details can be found here. There are edge cases we also take care of, and optimizations to preserve write performance of large bulk mutations. However, the optimizations does not affect the functionality else than that liveQueries may be triggered as false positives in certain times.
Rules for the querier function
- Don't call non-Dexie asynchronous API:s directly from it.
- If you really need to call a non-Dexie asynchronous API (such as webCrypto), wrap the returned promise through `Promise.resolve()` or Dexie.waitFor() before awaiting it.
Sample: Await non-dexie async calls in querier
import { db } from './db';
import Dexie, { liveQuery } from 'dexie';
const friendHashObservable = liveQuery (
async () => {
const friends = await db.friends.toArray();
const byteArray = new TextEncoder().encode(JSON.stringify(friends));
const digestBytes = await Dexie.waitFor(
crypto.subtle.digest('SHA-1', byteArray)
);
return digestBytes;
}
);
Fine grained observation
The observation is as fine-grained as it can possibly be - queries that would be affected by a modification will rerender - others not (with some exceptions - false positives happen but never false negatives). This is also true if your querier callback performs a series of awaited queries or multiple in parallel using Promise.all(). It can even contain if-statements or other conditional paths within it, determining additional queries to make before returning a final result - still, observation will function and never miss an update. No matter how simple or complex the query is - it will be monitored in detail so that if a single part of the query is affected by a change, the querier will be executed and the component will rerender.
Once again, the rule is that:
If a database change would affect the result of your querier, your querier callback will be re-executed and your observable will emit the new result.
Playgrounds
Table of Contents
- API Reference
- Access Control in Dexie Cloud
- Add demo users
- Add public data
- Authentication in Dexie Cloud
- Best Practices
- Building Addons
- Collection
- Collection.and()
- Collection.clone()
- Collection.count()
- Collection.delete()
- Collection.desc()
- Collection.distinct()
- Collection.each()
- Collection.eachKey()
- Collection.eachPrimaryKey()
- Collection.eachUniqueKey()
- Collection.filter()
- Collection.first()
- Collection.keys()
- Collection.last()
- Collection.limit()
- Collection.modify()
- Collection.offset()
- Collection.or()
- Collection.primaryKeys()
- Collection.raw()
- Collection.reverse()
- Collection.sortBy()
- Collection.toArray()
- Collection.uniqueKeys()
- Collection.until()
- Compound Index
- Consistency in Dexie Cloud
- Consistent add() operator
- Consistent remove() operator
- Consistent replacePrefix() operator
- Consuming Dexie as a module
- Custom Emails in Dexie Cloud
- DBCore
- DBCoreAddRequest
- DBCoreCountRequest
- DBCoreCursor
- DBCoreDeleteRangeRequest
- DBCoreDeleteRequest
- DBCoreGetManyRequest
- DBCoreGetRequest
- DBCoreIndex
- DBCoreKeyRange
- DBCoreMutateRequest
- DBCoreMutateResponse
- DBCoreOpenCursorRequest
- DBCorePutRequest
- DBCoreQuery
- DBCoreQueryRequest
- DBCoreQueryResponse
- DBCoreRangeType
- DBCoreSchema
- DBCoreTable
- DBCoreTableSchema
- DBCoreTransaction
- DBCoreTransactionMode
- DBPermissionSet
- Deprecations
- Derived Work
- Design
- Dexie Cloud API
- Dexie Cloud API Limits
- Dexie Cloud Best Practices
- Dexie Cloud CLI
- Dexie Cloud Docs
- Dexie Cloud REST API
- Dexie Cloud Web Hooks
- Dexie Constructor
- Dexie.AbortError
- Dexie.BulkError
- Dexie.ConstraintError
- Dexie.DataCloneError
- Dexie.DataError
- Dexie.DatabaseClosedError
- Dexie.IncompatiblePromiseError
- Dexie.InternalError
- Dexie.InvalidAccessError
- Dexie.InvalidArgumentError
- Dexie.InvalidStateError
- Dexie.InvalidTableError
- Dexie.MissingAPIError
- Dexie.ModifyError
- Dexie.NoSuchDatabaseErrorError
- Dexie.NotFoundError
- Dexie.Observable
- Dexie.Observable.DatabaseChange
- Dexie.OpenFailedError
- Dexie.PrematureCommitError
- Dexie.QuotaExceededError
- Dexie.ReadOnlyError
- Dexie.SchemaError
- Dexie.SubTransactionError
- Dexie.Syncable
- Dexie.Syncable.IDatabaseChange
- Dexie.Syncable.IPersistentContext
- Dexie.Syncable.ISyncProtocol
- Dexie.Syncable.StatusTexts
- Dexie.Syncable.Statuses
- Dexie.Syncable.registerSyncProtocol()
- Dexie.TimeoutError
- Dexie.TransactionInactiveError
- Dexie.UnknownError
- Dexie.UnsupportedError
- Dexie.UpgradeError()
- Dexie.VersionChangeError
- Dexie.VersionError
- Dexie.[table]
- Dexie.addons
- Dexie.async()
- Dexie.backendDB()
- Dexie.close()
- Dexie.currentTransaction
- Dexie.debug
- Dexie.deepClone()
- Dexie.defineClass()
- Dexie.delByKeyPath()
- Dexie.delete()
- Dexie.derive()
- Dexie.events()
- Dexie.exists()
- Dexie.extend()
- Dexie.fakeAutoComplete()
- Dexie.getByKeyPath()
- Dexie.getDatabaseNames()
- Dexie.hasFailed()
- Dexie.ignoreTransaction()
- Dexie.isOpen()
- Dexie.js
- Dexie.name
- Dexie.on()
- Dexie.on.blocked
- Dexie.on.close
- Dexie.on.error
- Dexie.on.populate
- Dexie.on.populate-(old-version)
- Dexie.on.ready
- Dexie.on.storagemutated
- Dexie.on.versionchange
- Dexie.open()
- Dexie.override()
- Dexie.semVer
- Dexie.setByKeyPath()
- Dexie.shallowClone()
- Dexie.spawn()
- Dexie.table()
- Dexie.tables
- Dexie.transaction()
- Dexie.transaction()-(old-version)
- Dexie.use()
- Dexie.verno
- Dexie.version
- Dexie.version()
- Dexie.vip()
- Dexie.waitFor()
- DexieCloudOptions
- DexieError
- Docs Home
- Download
- EntityTable
- Export and Import Database
- Get started with Dexie in Angular
- Get started with Dexie in React
- Get started with Dexie in Svelte
- Get started with Dexie in Vue
- Hello World
- How To Use the StorageManager API
- Inbound
- IndexSpec
- Indexable Type
- IndexedDB on Safari
- Invite
- Member
- Migrating existing DB to Dexie
- MultiEntry Index
- PersistedSyncState
- Privacy Policy
- Promise
- Promise.PSD
- Promise.catch()
- Promise.finally()
- Promise.on.error
- Promise.onuncatched
- Questions and Answers
- Realm
- Releasing Dexie
- Road Map
- Road Map: Dexie 5.0
- Road Map: Dexie Cloud
- Role
- Run Dexie Cloud on Own Servers
- Sharding and Scalability
- Simplify with yield
- Support Ukraine
- SyncState
- Table
- Table Schema
- Table.add()
- Table.bulkAdd()
- Table.bulkDelete()
- Table.bulkGet()
- Table.bulkPut()
- Table.bulkUpdate()
- Table.clear()
- Table.count()
- Table.defineClass()
- Table.delete()
- Table.each()
- Table.filter()
- Table.get()
- Table.hook('creating')
- Table.hook('deleting')
- Table.hook('reading')
- Table.hook('updating')
- Table.limit()
- Table.mapToClass()
- Table.name
- Table.offset()
- Table.orderBy()
- Table.put()
- Table.reverse()
- Table.schema
- Table.toArray()
- Table.toCollection()
- Table.update()
- Table.where()
- The main limitations of IndexedDB
- Transaction
- Transaction.abort()
- Transaction.on.abort
- Transaction.on.complete
- Transaction.on.error
- Transaction.table()
- Tutorial
- Typescript
- Typescript (old)
- Understanding the basics
- UserLogin
- Version
- Version.stores()
- Version.upgrade()
- WhereClause
- WhereClause.above()
- WhereClause.aboveOrEqual()
- WhereClause.anyOf()
- WhereClause.anyOfIgnoreCase()
- WhereClause.below()
- WhereClause.belowOrEqual()
- WhereClause.between()
- WhereClause.equals()
- WhereClause.equalsIgnoreCase()
- WhereClause.inAnyRange()
- WhereClause.noneOf()
- WhereClause.notEqual()
- WhereClause.startsWith()
- WhereClause.startsWithAnyOf()
- WhereClause.startsWithAnyOfIgnoreCase()
- WhereClause.startsWithIgnoreCase()
- db.cloud.configure()
- db.cloud.currentUser
- db.cloud.currentUserId
- db.cloud.events.syncComplete
- db.cloud.invites
- db.cloud.login()
- db.cloud.logout()
- db.cloud.options
- db.cloud.permissions()
- db.cloud.persistedSyncState
- db.cloud.roles
- db.cloud.schema
- db.cloud.sync()
- db.cloud.syncState
- db.cloud.userInteraction
- db.cloud.usingServiceWorker
- db.cloud.version
- db.cloud.webSocketStatus
- db.members
- db.realms
- db.roles
- db.syncable.connect()
- db.syncable.delete()
- db.syncable.disconnect()
- db.syncable.getOptions()
- db.syncable.getStatus()
- db.syncable.list()
- db.syncable.on('statusChanged')
- db.syncable.setFilter()
- dexie-cloud-addon
- dexie-react-hooks
- liveQuery()
- unhandledrejection-event
- useLiveQuery()
- useObservable()
- usePermissions()