Live Queries
Live queries automatically update when the underlying data changes, providing real-time reactive data for your applications.
Overview
Live queries in Routier allow you to subscribe to data changes and automatically receive updates when the underlying data is modified. This is perfect for building reactive UIs and real-time applications.
Quick Navigation
- Quick Reference
- Important: Callbacks vs Async
- Basic Live Queries
- Advanced Live Query Patterns
- Managing Live Queries
- Performance Considerations
- Common Patterns
- Best Practices
- Error Handling
- Related Topics
Quick Reference
| Method | Description | Example |
|---|---|---|
subscribe() | Enable live updates | ctx.products.subscribe().toArray(callback) |
defer() | Skip initial query | ctx.products.defer().subscribe().toArray(callback) |
unsubscribe() | Stop live updates | query.unsubscribe() |
Important: Callbacks vs Async
When using .subscribe(), you must use callback-based methods (not async methods):
// ✅ Correct: Use callbacks with .subscribe()
ctx.users.subscribe().toArray((result) => {
if (result.ok === "success") {
console.log(result.data);
}
});
// ❌ Incorrect: Cannot use async methods with .subscribe()
// This will NOT work:
const data = await ctx.users.subscribe().toArrayAsync();
The reason: subscriptions need to trigger the callback whenever data changes, which can’t be done with promises. Callbacks can be invoked at any time, making them perfect for reactive updates.
Basic Live Queries
Simple Live Query
// Create a live query that updates automatically
ctx.users.subscribe().toArray((result) => {
if (result.ok === "success") {
console.log(result.data); // Live data updates automatically
}
});
// The query will automatically update when users are added, updated, or removed
Live Query with Filtering
// Live query with filtering - updates when filtered data changes
ctx.users
.where((u) => u.isActive === true)
.subscribe()
.toArray((result) => {
if (result.ok === "success") {
console.log("Active users:", result.data);
}
});
// This will automatically update when:
// - New active users are added
// - Existing users become active/inactive
// - Active users are removed
Live Query with Sorting
// Live query with sorting - maintains sort order as data changes
ctx.users
.sort((u) => u.name)
.subscribe()
.toArray((result) => {
if (result.ok === "success") {
console.log("Sorted users:", result.data);
}
});
// This will automatically update and maintain alphabetical order when:
// - New users are added
// - User names are updated
// - Users are removed
Deferring the Initial Query
The .defer() method skips the first query execution only, then listens to all subsequent changes. This is useful when you only want to react to new data, not load existing data on initial setup.
How .defer() Works
When you use .defer(), the query:
- Sets up the subscription without executing the query immediately
- Waits for the first change event to occur
- Executes the query when the first change happens
- After the first change, behaves normally—executes on every subsequent change
Important: .defer() must be called before .subscribe():
// ✅ Correct: defer() before subscribe()
ctx.products
.defer()
.subscribe()
.toArray((result) => {
if (result.ok === "success") {
console.log("Data after first change:", result.data);
}
});
// ❌ Incorrect: subscribe() before defer() won't work as expected
ctx.products
.subscribe()
.defer()
.toArray((result) => {
// This will still execute the initial query
});
When to Use .defer()
Use .defer() when you want to:
- Skip initial data load: Only react to changes, not current state
- Prevent view computation on datastore creation: In views, prevent the derive function from executing immediately when the datastore is instantiated
- Activity feeds: Show only new items after a component mounts
- Notifications: Display only new notifications, not historical ones
- Chat messages: Show only new messages after joining a chat
Example: Activity Feed
// Skip first query, then show activities on changes
ctx.activities
.defer()
.subscribe()
.toArray((result) => {
if (result.ok === "success") {
// This callback is NOT called on initial setup (first execution skipped)
// It's called on the first change event
// It's called on every subsequent change (normal behavior)
console.log("Activities:", result.data);
}
});
Example: In Views
When creating views, use .defer() to prevent the view from computing immediately when the datastore is created:
commentsView = this.view(commentsViewSchema)
.derive((done) => {
// defer() prevents this query from executing when the datastore is created
// The view will only compute when comments actually change
const unsubscribe = this.comments
.defer()
.subscribe()
.toArray((response) => {
if (response.ok === "error") {
return done([]);
}
done(
response.data.map((x) => ({
id: `view:${x._id}`,
content: x.content,
// ... transform data
}))
);
});
return unsubscribe; // Return unsubscribe function for cleanup
})
.create();
Why use .defer() in views?
- Without
.defer(), the view would execute the query immediately when the datastore is instantiated - This can cause unnecessary computation if the view is created but not immediately needed
- With
.defer(), the view skips the first computation on creation, then computes on the first change and all subsequent changes
Comparison: With vs Without .defer()
// Without .defer() - Executes immediately
ctx.products.subscribe().toArray((result) => {
// ✅ Called immediately with current data
// ✅ Called again on every subsequent change
console.log("Products:", result.data);
});
// With .defer() - Skips first execution only
ctx.products
.defer()
.subscribe()
.toArray((result) => {
// ❌ NOT called on initial setup (first execution skipped)
// ✅ Called on first change event
// ✅ Called on every subsequent change (normal behavior)
console.log("Products after change:", result.data);
});
Advanced Live Query Patterns
Live Aggregation
// Live count that updates automatically
ctx.users.subscribe().count((result) => {
if (result.ok === "success") {
console.log("User count:", result.data);
}
});
// Live sum that updates automatically
ctx.products
.where((p) => p.inStock === true)
.subscribe()
.sum(
(result, selector) => {
if (result.ok === "success") {
console.log("Total value:", result.data);
}
},
(p) => p.price
);
Live Pagination
// Live paginated results
const pageSize = 10;
const currentPage = 0;
ctx.users
.skip(currentPage * pageSize)
.take(pageSize)
.subscribe()
.toArray((result) => {
if (result.ok === "success") {
console.log("Current page:", result.data);
}
});
// This will update when users are added/removed/modified
// affecting the current page
Live Single Item
// Live single item query
ctx.users
.sort((u) => u.createdAt)
.subscribe()
.firstOrUndefined((result) => {
if (result.ok === "success") {
console.log("First user:", result.data);
}
});
// This will update when the first user changes
Managing Live Queries
Unsubscribing
// Create a live query
const subscription = ctx.users.subscribe().toArray((result) => {
if (result.ok === "success") {
console.log(result.data);
}
});
// Later, unsubscribe to stop updates
subscription.unsubscribe();
Conditional Live Queries
// Only create live query if needed
let unsubscribe: (() => void) | null = null;
if (shouldUseLiveQuery) {
unsubscribe = ctx.users.subscribe().toArray((result) => {
if (result.ok === "success") {
// Handle data
}
}).unsubscribe;
} else {
ctx.users.toArrayAsync().then((data) => {
// Handle data
});
}
Performance Considerations
Efficient Live Queries
// Good: Apply filters before subscribing
ctx.products
.where((p) => p.price > 100)
.subscribe()
.toArray((result) => {
if (result.ok === "success") {
// Handle expensive products
}
});
// Less efficient: Subscribe to all data then filter
ctx.products.subscribe().toArray((result) => {
if (result.ok === "success") {
const expensiveProducts = result.data.filter((p) => p.price > 100);
// Handle filtered results
}
});
Memory Management
// Clean up live queries when components unmount
class UserComponent {
private unsubscribe: (() => void) | null = null;
initialize() {
const subscription = ctx.users.subscribe().toArray((result) => {
if (result.ok === "success") {
// Handle data
}
});
this.unsubscribe = subscription.unsubscribe;
}
destroy() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
}
Common Patterns
Real-time Dashboard
// Live dashboard with multiple live queries
ctx.users.subscribe().count((result) => {
if (result.ok === "success") {
console.log("Total users:", result.data);
}
});
ctx.products
.where((p) => p.inStock === true)
.subscribe()
.count((result) => {
if (result.ok === "success") {
console.log("Active products:", result.data);
}
});
ctx.products
.orderByDescending((p) => p.sales)
.take(5)
.subscribe()
.toArray((result) => {
if (result.ok === "success") {
console.log("Top sellers:", result.data);
}
});
Live Search Results
// Live search that updates as user types
const searchTerm = "john";
ctx.users
.where((u) => u.name.toLowerCase().includes(searchTerm.toLowerCase()))
.subscribe()
.toArray((result) => {
if (result.ok === "success") {
console.log("Search results:", result.data);
}
});
Live Notifications
// Live query for unread notifications
ctx.notifications
.where((n) => n.isRead === false)
.orderByDescending((n) => n.createdAt)
.subscribe()
.toArray((result) => {
if (result.ok === "success") {
console.log("Unread notifications:", result.data);
}
});
Best Practices
1. Use Live Queries for Real-time Data
// Good: Use live queries for data that changes frequently
ctx.messages.subscribe().toArray((result) => {
if (result.ok === "success") {
console.log("Messages:", result.data);
}
});
// Less useful: Static data doesn't need live queries
const staticConfig = await ctx.config.toArrayAsync();
2. Apply Filters Before Subscribing
// Good: Filter before subscribing to reduce tracked changes
ctx.users
.where((u) => u.isActive === true)
.subscribe()
.toArray((result) => {
if (result.ok === "success") {
// Handle active users
}
});
// Less efficient: Subscribe to all data, then filter in component
ctx.users.subscribe().toArray((result) => {
if (result.ok === "success") {
const activeUsers = result.data.filter((u) => u.isActive);
// Handle filtered results
}
});
3. Clean Up Subscriptions
// Always unsubscribe when done
const subscription = ctx.users.subscribe().toArray((result) => {
if (result.ok === "success") {
// Handle data
}
});
// Clean up
subscription.unsubscribe();
4. Use Appropriate Terminal Methods
// Use count() for counts
ctx.users.subscribe().count((result) => {
if (result.ok === "success") {
console.log("Count:", result.data);
}
});
// Use toArray() for lists
ctx.users.subscribe().toArray((result) => {
if (result.ok === "success") {
console.log("List:", result.data);
}
});
// Use firstOrUndefined() for single items
ctx.users.subscribe().firstOrUndefined((result) => {
if (result.ok === "success") {
console.log("First:", result.data);
}
});
Error Handling
Live Query Error Handling
ctx.users.subscribe().toArray((result) => {
if (result.ok === "success") {
// Handle live updates
} else {
console.error("Live query error:", result.error);
// Fallback to regular query
ctx.users.toArrayAsync().then((users) => {
// Handle fallback data
});
}
});
Related Topics
- Queries - Basic query operations
- State Management - Managing application state
- Data Manipulation - Modifying data