Use this file to discover all available pages before exploring further.
Watch queries, also known as live queries, are essential for building reactive apps where the UI automatically updates when the underlying data changes. PowerSync’s watch functionality allows you to listen for SQL query result changes and receive updates whenever the dependent tables are modified.
PowerSync supports the following basic watch queries based on your platform. These APIs return query results whenever the underlying tables change and are available across all SDKs.
JavaScript
Dart/Flutter
Kotlin
Swift
.NET
Rust
// The original watch method using the AsyncIterator pattern. This is the// foundational watch API that works across all JavaScript environments.async function* pendingLists(): AsyncIterable<string[]> { for await (const result of db.watch( `SELECT * FROM lists WHERE state = ?`, ['pending'] )) { yield result.rows?._array ?? []; }}
The db.watch() AsyncIterator and Callback methods above are only maintained for backwards compatibility. Use the improved db.query().watch() API instead (see Incremental Watch Queries below).
Use this method to watch for changes to the dependent tables of any SQL query:
StreamBuilder( stream: db.watch('SELECT * FROM lists WHERE state = ?', ['pending']), builder: (context, snapshot) { if (snapshot.hasData) { // TODO: implement your own UI here based on the result set return ...; } else { return const Center(child: CircularProgressIndicator()); } },)
Use this method to watch for changes to the dependent tables of any SQL query:
fun watchPendingLists(): Flow<List<ListItem>> = db.watch( "SELECT * FROM lists WHERE state = ?", listOf("pending"), ) { cursor -> ListItem( id = cursor.getString("id"), name = cursor.getString("name"), ) }
Use this method to watch for changes to the dependent tables of any SQL query:
func watchPendingLists() throws -> AsyncThrowingStream<[ListContent], Error> { try db.watch( sql: "SELECT * FROM lists WHERE state = ?", parameters: ["pending"], ) { cursor in try ListContent( id: cursor.getString(name: "id"), name: cursor.getString(name: "name"), ) }}
Use this method to watch for changes to the dependent tables of any SQL query:
// Define a result type with properties matching the schema columns (some columns omitted here for brevity)// public class ListResult { public string id; public string name; public string owner_id; ... }// Optional cancellation token to stop watchingvar cts = new CancellationTokenSource();// Register listener synchronously on the calling thread...var listener = db.Watch<ListResult>( "SELECT * FROM lists WHERE owner_id = ?", [ownerId], new SQLWatchOptions { Signal = cts.Token });// ...then listen to changes on another thread (or await foreach directly if already in an async context)_ = Task.Run(async () =>{ await foreach (var results in listener) { Console.WriteLine("Lists: "); foreach (var result in results) { Console.WriteLine($"{result.id}: {result.name}"); } }}, cts.Token);// To stop watching, cancel the token: cts.Cancel();
Use this method to watch for changes to the dependent tables of any SQL query:
async fn watch_pending_lists(db: &PowerSyncDatabase) -> Result<(), PowerSyncError> { let stream = db.watch_statement( "SELECT * FROM lists WHERE state = ?".to_string(), params!["pending"], |stmt, params| { let mut rows = stmt.query(params)?; let mut mapped = vec![]; while let Some(row) = rows.next()? { mapped.push(() /* TODO: Read row into list struct */) } Ok(mapped) }, ); let mut stream = pin!(stream); // Note: The stream is never-ending, so you probably want to call this in an independent async // task. while let Some(event) = stream.try_next().await? { // Update UI to display rows } Ok(())}
Basic watch queries can cause performance issues in UI frameworks like React because they return new data on every dependent table change, even when the actual data in the query hasn’t changed. This can lead to excessive re-renders as components receive updates unnecessarily.Incremental watch queries solve this by comparing result sets using configurable comparators and only emitting updates when the comparison detects actual data changes. These queries still query the SQLite database under the hood on each dependent table change, but compare the result sets and only yield results if a change has been made.
JavaScript Only: Incremental and differential watch queries are currently only available in the JavaScript SDKs starting from:
Web v1.25.0
React Native v1.23.1
Node.js v0.8.1
Basic Syntax:
db.query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }).watch();
WatchedQuery class that comes with a better API in that it includes loading, fetching and error states, supports multiple listeners, automatic cleanup on PowerSync close, and the new updateSettings() API for dynamic parameter changes. This is the preferred approach for JavaScript SDKs:
// Create an instance of a WatchedQueryconst pendingLists = db .query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }) .watch();// The registerListener method can be used multiple times to listen for updatesconst dispose = pendingLists.registerListener({ onData: (data) => { // This callback will be called whenever the data changes console.log('Data updated:', data); }, onStateChange: (state) => { // This callback will be called whenever the state changes // The state contains metadata about the query, such as isFetching, isLoading, etc. console.log('State changed:', state.error, state.isFetching, state.isLoading, state.data); }, onError: (error) => { // This callback will be called if the query fails console.error('Query error:', error); }});
React hooks that preserve object references for unchanged items and use row-level comparators to minimize re-renders:
// Use this when you want built-in state management plus// incremental updates for React components.const { data: pendingLists, isLoading, isFetching, error} = useQuery('SELECT * FROM lists WHERE state = ?', ['pending'], { rowComparator: { keyBy: (item) => item.id, compareBy: (item) => JSON.stringify(item) }});
Providing a rowComparator to the React hooks ensures that components only re-render when the query result actually changes. When combined with React memoization (e.g., React.memo) on row components that receive query row objects as props, this approach prevents unnecessary updates at the individual row component level, resulting in more efficient UI rendering.
The existing AsyncIterator and Callback db.watch() APIs also support incremental updates via a comparator option. Use these if you want to maintain the familiar patterns from the basic watch query API:
async function* pendingLists(): AsyncIterable<string[]> { for await (const result of db.watch('SELECT * FROM lists WHERE state = ?', ['pending'], { comparator: { checkEquality: (current, previous) => JSON.stringify(current) === JSON.stringify(previous) } })) { yield result.rows?._array ?? []; }}
Differential watch queries go a step further than incremental watched queries by computing and reporting diffs between result sets (added/removed/updated items) while preserving object references for unchanged items. This enables more precise UI updates.
JavaScript Only: Incremental and differential watch queries are currently only available in the JavaScript SDKs starting from:
Web v1.25.0
React Native v1.23.1
Node.js v0.8.1
For large result sets where re-running and comparing full query results becomes expensive, consider using trigger-based table diffs. See High Performance Diffs.
Basic syntax:
db.query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }).differentialWatch();
Use differential watch when you need to know exactly which items were added, removed, or updated rather than re-processing entire result sets:
// Create an instance of a WatchedQueryconst pendingLists = db .query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }) .differentialWatch();// The registerListener method can be used multiple times to listen for updatesconst dispose = pendingLists.registerListener({ onData: (data) => { // This callback will be called whenever the data changes console.log('Data updated:', data); }, onStateChange: (state) => { // This callback will be called whenever the state changes // The state contains metadata about the query, such as isFetching, isLoading, etc. console.log('State changed:', state.error, state.isFetching, state.isLoading, state.data); }, onError: (error) => { // This callback will be called if the query fails console.error('Query error:', error); }, onDiff: (diff) => { // This callback will be called whenever the data changes. console.log('Data updated:', diff.added, diff.updated); }});
By default, the differentialWatch() method uses a DEFAULT_ROW_COMPARATOR. This comparator identifies (keys) each row by its id column if present, or otherwise by the JSON string of the entire row. For row comparison, it uses the JSON string representation of the full row. This approach is generally safe and effective for most queries.For some queries, performance could be improved by supplying a custom rowComparator. Such as comparing by a hash column generated or stored in SQLite. These hashes currently require manual implementation.
const pendingLists = db .query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }) .differentialWatch({ rowComparator: { keyBy: (item) => item.id, compareBy: (item) => item._hash } });
The Yjs Document Collaboration Demo
app showcases the use of
differential watch queries. New document updates are passed to Yjs for consolidation as they are synced. See the
implementation
here
for more details.
Both incremental and differential queries use the new WatchedQuery class. This class, along with a new query method allows building instances of WatchedQuerys via the watch and differentialWatch methods:
Update query parameters to affect all listeners of the query:
// Updates to query parameters can be performed in a single place, affecting all listenerssharedListsQuery.updateSettings({ query: new GetAllQuery({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['canceled'] })});
When you need to share query instances across components or manage their lifecycle independently from component mounting, use the useWatchedQuerySubscription hook. This is ideal for global state management, query caching, or when multiple components need to listen to the same data:
// Managing the WatchedQuery externally can extend its lifecycle and allow in-memory caching between components.const pendingLists = db .query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }) .watch();// In the componentexport const MyComponent = () => { // In React one could import the `pendingLists` query or create a context provider for various queries const { data } = useWatchedQuerySubscription(pendingLists); return ( <div> {data.map((item) => ( <div key={item.id}>{item.name}</div> ))} </div> );};