Reactive query system that automatically updates when underlying data changes, following the TC39 Observable proposal.
Creates a reactive query that automatically re-executes when dependent data changes.
/**
* Creates a reactive query that automatically updates when data changes
* @param querier - Function that returns data or a Promise of data
* @returns Observable that emits updated results
*/
function liveQuery<T>(querier: () => T | Promise<T>): Observable<T>;
interface Observable<T> {
/** Subscribe to value changes */
subscribe(observer: Observer<T> | ((value: T) => void)): Subscription;
}
interface Observer<T> {
/** Called when new value is available */
next: (value: T) => void;
/** Called when an error occurs */
error?: (error: any) => void;
/** Called when observable completes (rarely used) */
complete?: () => void;
}
interface Subscription {
/** Stop receiving updates */
unsubscribe(): void;
}Usage Examples:
import { liveQuery } from "dexie";
// Basic live query
const friends$ = liveQuery(() => db.friends.toArray());
// Subscribe with function
friends$.subscribe(friends => {
console.log("Friends updated:", friends);
});
// Subscribe with observer object
friends$.subscribe({
next: friends => console.log("Friends:", friends),
error: err => console.error("Query failed:", err)
});
// Query with parameters
function getFriendsByAge(minAge: number) {
return liveQuery(() =>
db.friends.where("age").aboveOrEqual(minAge).toArray()
);
}
const adults$ = getFriendsByAge(18);
adults$.subscribe(adults => {
console.log(`${adults.length} adults found`);
});Live queries can include any database operations and will automatically track dependencies.
Usage Examples:
// Complex query with joins
const postsWithAuthors$ = liveQuery(async () => {
const posts = await db.posts.orderBy("createdAt").reverse().limit(10).toArray();
// Fetch authors for each post
const postsWithAuthors = await Promise.all(
posts.map(async post => {
const author = await db.users.get(post.authorId);
return { ...post, author };
})
);
return postsWithAuthors;
});
// Aggregation query
const statistics$ = liveQuery(async () => {
const [totalUsers, totalPosts, activeUsers] = await Promise.all([
db.users.count(),
db.posts.count(),
db.users.where("lastActive").above(Date.now() - 24 * 60 * 60 * 1000).count()
]);
return { totalUsers, totalPosts, activeUsers };
});
// Conditional query
const userDashboard$ = liveQuery(async () => {
const currentUser = await db.users.get(currentUserId);
if (!currentUser) return null;
const [posts, friends, messages] = await Promise.all([
db.posts.where("authorId").equals(currentUserId).toArray(),
db.friendships.where("userId").equals(currentUserId).toArray(),
db.messages.where("recipientId").equals(currentUserId).count()
]);
return { user: currentUser, posts, friends, unreadMessages: messages };
});Managing subscriptions for memory leaks prevention and lifecycle control.
Usage Examples:
// Store subscription for cleanup
let subscription: Subscription;
function startWatching() {
const friends$ = liveQuery(() => db.friends.toArray());
subscription = friends$.subscribe(friends => {
updateUI(friends);
});
}
function stopWatching() {
if (subscription) {
subscription.unsubscribe();
subscription = null;
}
}
// Auto-cleanup with component lifecycle
class FriendsComponent {
private subscription?: Subscription;
onMount() {
const friends$ = liveQuery(() => db.friends.toArray());
this.subscription = friends$.subscribe(friends => {
this.friends = friends;
});
}
onDestroy() {
this.subscription?.unsubscribe();
}
}
// Multiple subscriptions management
class Dashboard {
private subscriptions: Subscription[] = [];
onMount() {
// Subscribe to multiple live queries
const friends$ = liveQuery(() => db.friends.toArray());
const posts$ = liveQuery(() => db.posts.limit(10).toArray());
const stats$ = liveQuery(() => this.calculateStats());
this.subscriptions.push(
friends$.subscribe(friends => this.friends = friends),
posts$.subscribe(posts => this.posts = posts),
stats$.subscribe(stats => this.stats = stats)
);
}
onDestroy() {
// Cleanup all subscriptions
this.subscriptions.forEach(sub => sub.unsubscribe());
this.subscriptions = [];
}
private async calculateStats() {
return {
friendCount: await db.friends.count(),
postCount: await db.posts.count(),
messageCount: await db.messages.count()
};
}
}Handling errors in live queries and reactive updates.
Usage Examples:
// Error handling with observer
const friends$ = liveQuery(() => {
// This might fail if database is closed
return db.friends.toArray();
});
friends$.subscribe({
next: friends => {
console.log("Friends loaded:", friends.length);
},
error: error => {
console.error("Failed to load friends:", error);
if (error.name === "DatabaseClosedError") {
// Handle database closed scenario
reopenDatabase();
}
}
});
// Graceful degradation
const robustQuery$ = liveQuery(async () => {
try {
const friends = await db.friends.toArray();
return { success: true, data: friends, error: null };
} catch (error) {
console.warn("Query failed, returning empty result:", error);
return { success: false, data: [], error: error.message };
}
});
robustQuery$.subscribe(result => {
if (result.success) {
updateUI(result.data);
} else {
showError(result.error);
}
});
// Retry on failure
function createResilientQuery<T>(queryFn: () => Promise<T>, retries = 3): Observable<T> {
return liveQuery(async () => {
let lastError;
for (let i = 0; i <= retries; i++) {
try {
return await queryFn();
} catch (error) {
lastError = error;
if (i < retries) {
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
throw lastError;
});
}Live queries integrate well with popular frontend frameworks.
// Using dexie-react-hooks
import { useLiveQuery } from "dexie-react-hooks";
function FriendsComponent() {
const friends = useLiveQuery(() => db.friends.toArray(), []);
const friendCount = useLiveQuery(() => db.friends.count(), 0);
if (!friends) return <div>Loading...</div>;
return (
<div>
<h2>Friends ({friendCount})</h2>
{friends.map(friend => (
<div key={friend.id}>{friend.name} - {friend.age}</div>
))}
</div>
);
}
// Custom React hook
function useTodos(completed?: boolean) {
return useLiveQuery(async () => {
let query = db.todos.orderBy("createdAt").reverse();
if (completed !== undefined) {
query = query.filter(todo => todo.completed === completed);
}
return query.toArray();
}, [completed]);
}// Vue 3 Composition API
import { ref, onUnmounted } from "vue";
function useLiveQuery<T>(queryFn: () => Promise<T>, defaultValue: T) {
const data = ref(defaultValue);
const loading = ref(true);
const error = ref(null);
const subscription = liveQuery(queryFn).subscribe({
next: (result) => {
data.value = result;
loading.value = false;
error.value = null;
},
error: (err) => {
error.value = err;
loading.value = false;
}
});
onUnmounted(() => {
subscription.unsubscribe();
});
return { data, loading, error };
}
// Usage in Vue component
export default {
setup() {
const { data: friends, loading, error } = useLiveQuery(
() => db.friends.toArray(),
[]
);
return { friends, loading, error };
}
};// Svelte store integration
import { readable } from "svelte/store";
function createLiveQueryStore<T>(queryFn: () => Promise<T>, initialValue: T) {
return readable(initialValue, (set) => {
const subscription = liveQuery(queryFn).subscribe(set);
return () => subscription.unsubscribe();
});
}
// Usage in Svelte component
const friends = createLiveQueryStore(() => db.friends.toArray(), []);
const friendCount = createLiveQueryStore(() => db.friends.count(), 0);
// In component template:
// {#each $friends as friend}
// <div>{friend.name}</div>
// {/each}Optimizing live queries for better performance.
Usage Examples:
// Avoid creating new live queries in render loops
// Bad:
function Component({ userId }) {
const posts = liveQuery(() => db.posts.where("authorId").equals(userId).toArray());
// Creates new query on every render
}
// Good:
function Component({ userId }) {
const posts = useLiveQuery(() =>
db.posts.where("authorId").equals(userId).toArray(),
[userId] // Dependency array
);
}
// Use specific queries instead of filtering in memory
// Bad:
const activeUsers$ = liveQuery(async () => {
const allUsers = await db.users.toArray();
return allUsers.filter(user => user.active);
});
// Good:
const activeUsers$ = liveQuery(() =>
db.users.where("active").equals(1).toArray()
);
// Debounce rapid updates
function createDebouncedLiveQuery<T>(
queryFn: () => Promise<T>,
delay: number = 100
): Observable<T> {
let timeoutId: NodeJS.Timeout;
let lastResult: T;
return liveQuery(async () => {
const result = await queryFn();
return new Promise<T>((resolve) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
lastResult = result;
resolve(result);
}, delay);
});
});
}
// Limit query scope
// Instead of querying all data and filtering client-side,
// use indexed queries to limit database reads
const recentPosts$ = liveQuery(() =>
db.posts
.where("createdAt")
.above(Date.now() - 7 * 24 * 60 * 60 * 1000) // Last 7 days
.limit(50)
.toArray()
);Complex reactive patterns using live queries.
Usage Examples:
// Computed live queries (derived data)
const friendStats$ = liveQuery(async () => {
const friends = await db.friends.toArray();
const ageGroups = friends.reduce((acc, friend) => {
const ageGroup = Math.floor(friend.age / 10) * 10;
acc[ageGroup] = (acc[ageGroup] || 0) + 1;
return acc;
}, {} as Record<number, number>);
return {
total: friends.length,
averageAge: friends.reduce((sum, f) => sum + f.age, 0) / friends.length,
ageGroups
};
});
// Combining multiple live queries
function combineLatest<T, U>(
obs1: Observable<T>,
obs2: Observable<U>
): Observable<[T, U]> {
return new Observable(observer => {
let value1: T, value2: U;
let has1 = false, has2 = false;
const sub1 = obs1.subscribe(v => {
value1 = v;
has1 = true;
if (has2) observer.next([value1, value2]);
});
const sub2 = obs2.subscribe(v => {
value2 = v;
has2 = true;
if (has1) observer.next([value1, value2]);
});
return () => {
sub1.unsubscribe();
sub2.unsubscribe();
};
});
}
// Usage
const friends$ = liveQuery(() => db.friends.toArray());
const posts$ = liveQuery(() => db.posts.toArray());
const combined$ = combineLatest(friends$, posts$);
combined$.subscribe(([friends, posts]) => {
console.log(`${friends.length} friends, ${posts.length} posts`);
});
// Transform live query results
function mapLiveQuery<T, U>(
source: Observable<T>,
mapper: (value: T) => U
): Observable<U> {
return new Observable(observer => {
return source.subscribe({
next: value => observer.next(mapper(value)),
error: err => observer.error(err),
complete: () => observer.complete()
});
});
}
// Usage
const friendNames$ = mapLiveQuery(
liveQuery(() => db.friends.toArray()),
friends => friends.map(f => f.name)
);