From 87d46336690cf94044eb234367a8030e22454cbe Mon Sep 17 00:00:00 2001 From: benitav Date: Thu, 10 Jul 2025 11:54:32 +0200 Subject: [PATCH 01/13] init --- docs.json | 1 + usage/use-case-examples.mdx | 1 + usage/use-case-examples/watch-queries.mdx | 367 ++++++++++++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 usage/use-case-examples/watch-queries.mdx diff --git a/docs.json b/docs.json index b5784660..8beb2737 100644 --- a/docs.json +++ b/docs.json @@ -184,6 +184,7 @@ "usage/use-case-examples/data-encryption", "usage/use-case-examples/full-text-search", "usage/use-case-examples/infinite-scrolling", + "usage/use-case-examples/watch-queries", "usage/use-case-examples/offline-only-usage", "usage/use-case-examples/postgis", "usage/use-case-examples/prioritized-sync", diff --git a/usage/use-case-examples.mdx b/usage/use-case-examples.mdx index 403405f1..5c513d69 100644 --- a/usage/use-case-examples.mdx +++ b/usage/use-case-examples.mdx @@ -16,6 +16,7 @@ The following examples are available to help you get started with specific use c + diff --git a/usage/use-case-examples/watch-queries.mdx b/usage/use-case-examples/watch-queries.mdx new file mode 100644 index 00000000..c4783e97 --- /dev/null +++ b/usage/use-case-examples/watch-queries.mdx @@ -0,0 +1,367 @@ +--- +title: "Live Queries / Watch Queries" +description: "Subscribe to real-time data changes with reactive live queries" +--- + +Live queries, also known as watch queries, are essential for building reactive apps where the UI automatically updates when the underlying data changes. PowerSync's watch functionality allows you to subscribe to SQL query results and receive updates whenever the dependent tables are modified. + +## Overview + +PowerSync provides three types of watch queries. All methods internally query SQLite whenever dependent tables change, but they expose results differently: + +- **`db.watch()`** - The original API that returns raw query results as new Array of objects every time +- **`db.query().watch()`** - Smart stateful representation with result comparison. Only exposes results when changes are detected +- **`db.query().differentialWatch()`** - Advanced diffing that tracks individual items and preserves object references. Perfect for React and provides detailed change information + + +**JavaScript Only**: The `db.query()` watch methods are currently available only in JavaScript SDKs. + + +## Basic Watch Queries + +Here are examples of how to use basic watch queries across different PowerSync client SDKs: + + + +```javascript JavaScript/Web +// Watch changes to the lists table +const abortController = new AbortController(); + +export const function watchLists = (onUpdate) => { + for await (const update of db.watch( + 'SELECT * from lists', + [], + { signal: abortController.signal } + ) + ) { + onUpdate(update); + } +} +``` + +```javascript React Native +// Using AsyncIterable pattern for watching data changes +async *todoIds(): AsyncIterable { + for await (const result of db.watch( + `SELECT id FROM todos WHERE list_id = ? AND completed = ?`, + [listId, false] + )) { + yield result.rows?._array.map((r) => r.id) ?? []; + } +} + +// Using callback pattern for watching data changes (SDK v1.3.3+) +todoIds(onResult: (ids: string[]) => void): void { + db.watch( + `SELECT id FROM todos WHERE list_id = ? AND completed = ?`, + [listId, false], + { + onResult: (result) => { + onResult(result.rows?._array.map((r) => r.id) ?? []); + } + } + ); +} +``` + +```dart Flutter/Dart +import 'package:flutter/material.dart'; + +class TodosWidget extends StatelessWidget { + const TodosWidget({super.key}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + // You can watch any SQL query + stream: db.watch('SELECT * FROM todos WHERE list_id = ? ORDER BY id ASC', [listId]), + builder: (context, snapshot) { + if (snapshot.hasData) { + // TODO: implement your own UI here based on the result set + return ...; + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ); + } +} +``` + +```kotlin Kotlin Multiplatform +// You can watch any SQL query +fun watchTodos(): Flow> { + // TODO: implement your UI based on the result set + return database.watch("SELECT * FROM todos WHERE list_id = ?", listId, mapper = { cursor -> + Todo( + id = cursor.getString("id"), + description = cursor.getString("description"), + completed = cursor.getBoolean("completed") + ) + }) +} +``` + +```swift Swift +// Watch for changes to the lists table +func watchLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { + do { + for try await lists in try self.db.watch( + sql: "SELECT * FROM \(LISTS_TABLE)", + parameters: [], + mapper: { cursor in + try ListContent( + id: cursor.getString(name: "id"), + name: cursor.getString(name: "name"), + createdAt: cursor.getString(name: "created_at"), + ownerId: cursor.getString(name: "owner_id") + ) + } + ) { + callback(lists) + } + } catch { + print("Error in watch: \(error)") + } +} +``` + +```csharp C#/.NET +// Use db.Watch() to watch queries for changes: +await db.Watch("SELECT * FROM lists", null, new WatchHandler +{ + OnResult = (results) => + { + Console.WriteLine("Results: "); + foreach (var result in results) + { + Console.WriteLine(result.id + ":" + result.name); + } + }, + OnError = (error) => + { + Console.WriteLine("Error: " + error.Message); + } +}); +``` + + + + +## Incremental Watch Queries + +Incremental watch queries solve the performance problems of basic watch queries by implementing intelligent change detection and granular updates. + + +**JavaScript Only**: Incremental watch queries are currently available only in the JavaScript SDKs. + + +### The WatchedQuery Class + +All incremental queries use the new `WatchedQuery` class, which provides advanced features: + +- Automatically reprocesses itself if the PowerSync schema has been updated with updateSchema +- Automatically closes itself when the PowerSync client has been closed. +- Allows for the query parameters to be updated after instantiation. +- Allows shared listening to state changes. + + +### 1. Comparison Watch Queries + +Comparison based queries behave similar to basic watched queries. These queries still query the SQLite DB under the hood on each dependant table change, but they compare the result set and only incrementally yield results if a change has been made. The latest query result is yielded as the result set. + +`db.query().watch()` method + +```javascript +// Comparison-based watch query - exits early when change detected +const query = db.query({ + sql: 'SELECT * FROM todos WHERE completed = ?', + parameters: [false] +}).watch(); + +// The registerListener method can be used multiple times to listen for updates +const dispose = query.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); + } +}); +``` + +### 2. Differential Watch Queries + +Differential queries watch a SQL query and report detailed information on the changes between result sets. This gives additional information such as the added, removed, updated rows between result set changes. + +`db.query().differentialWatch()` method + +```javascript +// Create an instance of a WatchedQuery +const listsQuery = db + .query({ + sql: ` + SELECT + lists.*, + COUNT(todos.id) AS total_tasks, + SUM( + CASE + WHEN todos.completed = true THEN 1 + ELSE 0 + END + ) as completed_tasks + FROM + lists + LEFT JOIN todos ON lists.id = todos.list_id + GROUP BY + lists.id; + ` + }) + .differentialWatch(); + +// The registerListener method can be used multiple times to listen for updates +// The returned dispose function can be used to unsubscribe from the updates +const disposeSubscriber = listsQuery.registerListener({ + onData: (data) => { + // This callback will be called whenever the data changes + // The data is the result of the executor + 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); + } +}); +``` + +**Custom Differentiator Logic:** + +```javascript +// The incremental logic is customizable with custom diff logic +const watch = db + .query({ + sql: ` + SELECT + * + FROM + todos + `, + mapper: (raw) => { + return { + id: raw.id as string, + description: raw.description as string + }; + } + }) + .differentialWatch({ + differentiator: { + identify: (item) => item.id, + compareBy: (item) => JSON.stringify(item) + } + }); +``` + +### Advanced Features + +**Custom Differentiator Logic:** + +The incremental logic is customizable with custom diff logic: + +```javascript +const watch = db + .query({ + sql: 'SELECT * FROM todos', + mapper: (raw) => ({ + id: raw.id as string, + description: raw.description as string + }) + }) + .differentialWatch({ + differentiator: { + identify: (item) => item.id, + compareBy: (item) => JSON.stringify(item) + } + }); +``` + +### Query Sharing + +`WatchedQuery` instances can be shared across components: + +```javascript +// Create a shared query instance +const sharedTodosQuery = db + .query({ sql: 'SELECT * FROM todos WHERE list_id = ?', parameters: [listId] }) + .watch(); + +// Multiple components can listen to the same query +const dispose1 = sharedTodosQuery.registerListener({ + onData: (data) => updateTodosList(data) +}); + +const dispose2 = sharedTodosQuery.registerListener({ + onData: (data) => updateTodosCount(data.length) +}); +``` + +### Dynamic Parameter Updates + +Update query parameters to affect all subscribers: + +```javascript +// Updates to query parameters can be performed in a single place, affecting all subscribers +watch.updateSettings({ + query: new GetAllQuery({ sql: `SELECT * FROM todos OFFSET ? LIMIT 100`, parameters: [newOffset] }) +}); +``` + +### Performance + +Incremental watch queries improve rendering performance: +- benchmarks show that incremental updates with differential queries coupled with React memoization render 60-80% faster +- incremental updates only render the newly added item widgets, while the standard query methods re-render the entire widget + + +### React Integration + +Use with React hooks: + +```javascript +import { useQuery } from '@powersync/react'; + +function TodosList() { + const { data: todos, isLoading, error } = useQuery( + 'SELECT * FROM todos WHERE completed = ?', + [false] + ); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return ( +
+ {todos.map(todo => ( + + ))} +
+ ); +} +``` From 6ad2a0015e6a8fc0425db8955062edb55096719f Mon Sep 17 00:00:00 2001 From: benitav Date: Thu, 10 Jul 2025 12:46:04 +0200 Subject: [PATCH 02/13] restructure --- usage/use-case-examples/watch-queries.mdx | 45 +++++------------------ 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/usage/use-case-examples/watch-queries.mdx b/usage/use-case-examples/watch-queries.mdx index c4783e97..01e51299 100644 --- a/usage/use-case-examples/watch-queries.mdx +++ b/usage/use-case-examples/watch-queries.mdx @@ -150,7 +150,10 @@ await db.Watch("SELECT * FROM lists", null, new WatchHandler ## Incremental Watch Queries -Incremental watch queries solve the performance problems of basic watch queries by implementing intelligent change detection and granular updates. +Basic watch queries can cause performance issues in UI frameworks like React because they return new arrays and objects on every change, even when the actual data hasn't changed. This leads to excessive re-renders as components can't distinguish between actual data changes and reference changes. + +Incremental watch queries address this problem by handling result sets in smarter ways - comparing result sets to detect actual changes, only emitting updates when data has truly changed, preserving object references when data is unchanged, and providing granular update information to minimize unnecessary re-renders. + **JavaScript Only**: Incremental watch queries are currently available only in the JavaScript SDKs. @@ -252,33 +255,6 @@ const disposeSubscriber = listsQuery.registerListener({ }); ``` -**Custom Differentiator Logic:** - -```javascript -// The incremental logic is customizable with custom diff logic -const watch = db - .query({ - sql: ` - SELECT - * - FROM - todos - `, - mapper: (raw) => { - return { - id: raw.id as string, - description: raw.description as string - }; - } - }) - .differentialWatch({ - differentiator: { - identify: (item) => item.id, - compareBy: (item) => JSON.stringify(item) - } - }); -``` - ### Advanced Features **Custom Differentiator Logic:** @@ -333,13 +309,6 @@ watch.updateSettings({ }); ``` -### Performance - -Incremental watch queries improve rendering performance: -- benchmarks show that incremental updates with differential queries coupled with React memoization render 60-80% faster -- incremental updates only render the newly added item widgets, while the standard query methods re-render the entire widget - - ### React Integration Use with React hooks: @@ -365,3 +334,9 @@ function TodosList() { ); } ``` + +### Performance + +Incremental watch queries improve rendering performance: +- benchmarks show that incremental updates with differential queries coupled with React memoization render 60-80% faster +- incremental updates only render the newly added item widgets, while the standard query methods re-render the entire widget \ No newline at end of file From 28bff672547a1da0e136955dc8ebb8c02c34e60a Mon Sep 17 00:00:00 2001 From: benitav Date: Thu, 10 Jul 2025 16:12:13 +0200 Subject: [PATCH 03/13] Apply suggestions from code review Co-authored-by: stevensJourney <51082125+stevensJourney@users.noreply.github.com> --- usage/use-case-examples/watch-queries.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usage/use-case-examples/watch-queries.mdx b/usage/use-case-examples/watch-queries.mdx index 01e51299..82f88eac 100644 --- a/usage/use-case-examples/watch-queries.mdx +++ b/usage/use-case-examples/watch-queries.mdx @@ -150,7 +150,7 @@ await db.Watch("SELECT * FROM lists", null, new WatchHandler ## Incremental Watch Queries -Basic watch queries can cause performance issues in UI frameworks like React because they return new arrays and objects on every change, even when the actual data hasn't changed. This leads to excessive re-renders as components can't distinguish between actual data changes and reference changes. +Basic watch queries can cause performance issues in UI frameworks like React because they return new arrays and objects on every dependent table change, even when the actual data hasn't changed. This can lead to excessive re-renders as components receive new props unnecessarily. Incremental watch queries address this problem by handling result sets in smarter ways - comparing result sets to detect actual changes, only emitting updates when data has truly changed, preserving object references when data is unchanged, and providing granular update information to minimize unnecessary re-renders. @@ -337,6 +337,6 @@ function TodosList() { ### Performance -Incremental watch queries improve rendering performance: +Incremental watch queries can improve rendering performance: - benchmarks show that incremental updates with differential queries coupled with React memoization render 60-80% faster - incremental updates only render the newly added item widgets, while the standard query methods re-render the entire widget \ No newline at end of file From 18879baa39112963ac0b5302323042315766ebb8 Mon Sep 17 00:00:00 2001 From: benitav Date: Fri, 11 Jul 2025 14:22:06 +0200 Subject: [PATCH 04/13] Improved code snippets --- usage/use-case-examples/watch-queries.mdx | 45 ++++++++++++----------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/usage/use-case-examples/watch-queries.mdx b/usage/use-case-examples/watch-queries.mdx index 82f88eac..086d3b7c 100644 --- a/usage/use-case-examples/watch-queries.mdx +++ b/usage/use-case-examples/watch-queries.mdx @@ -89,37 +89,38 @@ class TodosWidget extends StatelessWidget { ``` ```kotlin Kotlin Multiplatform -// You can watch any SQL query -fun watchTodos(): Flow> { - // TODO: implement your UI based on the result set - return database.watch("SELECT * FROM todos WHERE list_id = ?", listId, mapper = { cursor -> +// Watch for changes to the todos table +fun watchTodos(): Flow> = + database.watch( + "SELECT * FROM todos WHERE list_id = ?", + listOf(listId), + ) { cursor -> Todo( id = cursor.getString("id"), description = cursor.getString("description"), - completed = cursor.getBoolean("completed") + completed = cursor.getBoolean("completed"), ) - }) -} + } ``` ```swift Swift // Watch for changes to the lists table func watchLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { do { - for try await lists in try self.db.watch( - sql: "SELECT * FROM \(LISTS_TABLE)", - parameters: [], - mapper: { cursor in - try ListContent( - id: cursor.getString(name: "id"), - name: cursor.getString(name: "name"), - createdAt: cursor.getString(name: "created_at"), - ownerId: cursor.getString(name: "owner_id") - ) - } - ) { - callback(lists) - } + // Actually consume the stream to trigger the error + for try await result in try database.watch( + sql: "SELECT * FROM lists", + parameters: [], + ) { cursor in + try ListContent( + id: cursor.getString(name: "id"), + name: cursor.getString(name: "name"), + createdAt: cursor.getString(name: "created_at"), + ownerId: cursor.getString(name: "owner_id") + ) + } { + // process result here + } } catch { print("Error in watch: \(error)") } @@ -127,7 +128,7 @@ func watchLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { ``` ```csharp C#/.NET -// Use db.Watch() to watch queries for changes: +// Watch for changes to the lists table await db.Watch("SELECT * FROM lists", null, new WatchHandler { OnResult = (results) => From 474554edb38f12b205c7ba4b342deddc2781a1a6 Mon Sep 17 00:00:00 2001 From: benitav Date: Mon, 14 Jul 2025 11:30:51 +0200 Subject: [PATCH 05/13] reword --- usage/use-case-examples/watch-queries.mdx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/usage/use-case-examples/watch-queries.mdx b/usage/use-case-examples/watch-queries.mdx index 086d3b7c..92c12828 100644 --- a/usage/use-case-examples/watch-queries.mdx +++ b/usage/use-case-examples/watch-queries.mdx @@ -9,9 +9,12 @@ Live queries, also known as watch queries, are essential for building reactive a PowerSync provides three types of watch queries. All methods internally query SQLite whenever dependent tables change, but they expose results differently: -- **`db.watch()`** - The original API that returns raw query results as new Array of objects every time -- **`db.query().watch()`** - Smart stateful representation with result comparison. Only exposes results when changes are detected -- **`db.query().differentialWatch()`** - Advanced diffing that tracks individual items and preserves object references. Perfect for React and provides detailed change information +- **`db.watch()`** - The original API that returns raw query results as new Array of objects every time when the underlying tables change +- **`db.query().watch()`** - Still query the SQLite DB under the hood on each dependant table change, but they compare the result set and only incrementally yield results if a change has been made. The latest query result is yielded as the result set. + - Basically: It's smarter than the above in that it knows if something changed +- **`db.query().differentialWatch()`** - Instead of only knowing if data changed, it tells you what changed: + - Added, removed, updated, and unchanged rows. + - Preserves object references for unchanged rows—so in frameworks like React, only changed rows actually update. **JavaScript Only**: The `db.query()` watch methods are currently available only in JavaScript SDKs. @@ -153,7 +156,7 @@ await db.Watch("SELECT * FROM lists", null, new WatchHandler Basic watch queries can cause performance issues in UI frameworks like React because they return new arrays and objects on every dependent table change, even when the actual data hasn't changed. This can lead to excessive re-renders as components receive new props unnecessarily. -Incremental watch queries address this problem by handling result sets in smarter ways - comparing result sets to detect actual changes, only emitting updates when data has truly changed, preserving object references when data is unchanged, and providing granular update information to minimize unnecessary re-renders. +Incremental watch queries address this problem by handling result sets in smarter ways - comparing result sets to detect actual changes, only emitting updates when data has actually changed, preserving object references when data is unchanged, and providing granular update information to minimize unnecessary re-renders. @@ -164,7 +167,7 @@ Incremental watch queries address this problem by handling result sets in smarte All incremental queries use the new `WatchedQuery` class, which provides advanced features: -- Automatically reprocesses itself if the PowerSync schema has been updated with updateSchema +- Automatically reprocesses itself if the PowerSync schema has been updated with `updateSchema`. - Automatically closes itself when the PowerSync client has been closed. - Allows for the query parameters to be updated after instantiation. - Allows shared listening to state changes. From aeebbbff618f26fef6a5fb848978b5c2e5c149da Mon Sep 17 00:00:00 2001 From: benitav Date: Tue, 22 Jul 2025 14:01:58 +0200 Subject: [PATCH 06/13] restructure --- usage/use-case-examples/watch-queries.mdx | 455 ++++++++++++---------- 1 file changed, 240 insertions(+), 215 deletions(-) diff --git a/usage/use-case-examples/watch-queries.mdx b/usage/use-case-examples/watch-queries.mdx index 92c12828..2caf880c 100644 --- a/usage/use-case-examples/watch-queries.mdx +++ b/usage/use-case-examples/watch-queries.mdx @@ -5,189 +5,199 @@ description: "Subscribe to real-time data changes with reactive live queries" Live queries, also known as watch queries, are essential for building reactive apps where the UI automatically updates when the underlying data changes. PowerSync's watch functionality allows you to subscribe to SQL query results and receive updates whenever the dependent tables are modified. -## Overview +- todo: link to this page from relevant sections in SDK references -PowerSync provides three types of watch queries. All methods internally query SQLite whenever dependent tables change, but they expose results differently: +# Overview -- **`db.watch()`** - The original API that returns raw query results as new Array of objects every time when the underlying tables change -- **`db.query().watch()`** - Still query the SQLite DB under the hood on each dependant table change, but they compare the result set and only incrementally yield results if a change has been made. The latest query result is yielded as the result set. - - Basically: It's smarter than the above in that it knows if something changed -- **`db.query().differentialWatch()`** - Instead of only knowing if data changed, it tells you what changed: - - Added, removed, updated, and unchanged rows. - - Preserves object references for unchanged rows—so in frameworks like React, only changed rows actually update. +PowerSync provides multiple approaches to watching queries, each designed for different use cases and performance requirements: - -**JavaScript Only**: The `db.query()` watch methods are currently available only in JavaScript SDKs. - +1. **Basic Watch Queries** - These queries work across all SDKs, providing real-time updates when dependent tables change +2. **Comparison Watch Queries** - Only emit updates when data actually changes, preventing unnecessary re-renders +3. **Differential Watch Queries** - Provide detailed information about what specifically changed between result sets -## Basic Watch Queries +Choose the approach that best fits your platform and performance needs. -Here are examples of how to use basic watch queries across different PowerSync client SDKs: +# Basic Watch Queries by Platform - +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 for backwards compatibility. -```javascript JavaScript/Web -// Watch changes to the lists table -const abortController = new AbortController(); + + -export const function watchLists = (onUpdate) => { - for await (const update of db.watch( - 'SELECT * from lists', - [], - { signal: abortController.signal } - ) - ) { - onUpdate(update); - } -} -``` +The original watch method using AsyncIterator pattern. This is the foundational watch API that works across all JavaScript environments and is being maintained for backwards compatibility. -```javascript React Native -// Using AsyncIterable pattern for watching data changes -async *todoIds(): AsyncIterable { +```javascript +async function* pendingLists(): AsyncIterable { for await (const result of db.watch( - `SELECT id FROM todos WHERE list_id = ? AND completed = ?`, - [listId, false] + `SELECT * FROM lists WHERE state = ?`, + ['pending'] )) { - yield result.rows?._array.map((r) => r.id) ?? []; + yield result.rows?._array ?? []; } } +``` + + + + +The callback-based watch method that doesn't require AsyncIterator polyfills. Use this approach when you need React Native compatibility or prefer synchronous method signatures: -// Using callback pattern for watching data changes (SDK v1.3.3+) -todoIds(onResult: (ids: string[]) => void): void { +```javascript +const pendingLists = (onResult: (lists: any[]) => void): void => { db.watch( - `SELECT id FROM todos WHERE list_id = ? AND completed = ?`, - [listId, false], + 'SELECT * FROM lists WHERE state = ?', + ['pending'], { - onResult: (result) => { - onResult(result.rows?._array.map((r) => r.id) ?? []); + onResult: (result: any) => { + onResult(result.rows?._array ?? []); } } ); } ``` -```dart Flutter/Dart -import 'package:flutter/material.dart'; - -class TodosWidget extends StatelessWidget { - const TodosWidget({super.key}); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - // You can watch any SQL query - stream: db.watch('SELECT * FROM todos WHERE list_id = ? ORDER BY id ASC', [listId]), - builder: (context, snapshot) { - if (snapshot.hasData) { - // TODO: implement your own UI here based on the result set - return ...; - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - ); - } -} -``` + + -```kotlin Kotlin Multiplatform -// Watch for changes to the todos table -fun watchTodos(): Flow> = - database.watch( - "SELECT * FROM todos WHERE list_id = ?", - listOf(listId), - ) { cursor -> - Todo( - id = cursor.getString("id"), - description = cursor.getString("description"), - completed = cursor.getBoolean("completed"), - ) - } +React hook that combines watch functionality with built-in loading, fetching, and error states. Use this when you need convenient state management without React Suspense: + +```javascript +const { data: pendingLists, isLoading, isFetching, error } = useQuery( + 'SELECT * FROM lists WHERE state = ?', + ['pending'] +); ``` -```swift Swift -// Watch for changes to the lists table -func watchLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { - do { - // Actually consume the stream to trigger the error - for try await result in try database.watch( - sql: "SELECT * FROM lists", - parameters: [], - ) { cursor in - try ListContent( - id: cursor.getString(name: "id"), - name: cursor.getString(name: "name"), - createdAt: cursor.getString(name: "created_at"), - ownerId: cursor.getString(name: "owner_id") - ) - } { - // process result here - } - } catch { - print("Error in watch: \(error)") - } -} + + + +React Suspense-based hook that automatically handles loading and error states through Suspense boundaries. Use this when you want to leverage React's concurrent features and avoid manual state handling: + +```javascript +const { data: pendingLists } = useSuspenseQuery( + 'SELECT * FROM lists WHERE state = ?', + ['pending'] +); ``` -```csharp C#/.NET -// Watch for changes to the lists table -await db.Watch("SELECT * FROM lists", null, new WatchHandler -{ - OnResult = (results) => - { - Console.WriteLine("Results: "); - foreach (var result in results) - { - Console.WriteLine(result.id + ":" + result.name); - } - }, - OnError = (error) => - { - Console.WriteLine("Error: " + error.Message); - } -}); + + + +Vue composition API hook with built-in loading, fetching, and error states. Use this for reactive watch queries in Vue applications: + +```javascript +const { data: pendingLists, isLoading, isFetching, error } = useQuery( + 'SELECT * FROM lists WHERE state = ?', + ['pending'] +); ``` - + + +todo -## Incremental Watch Queries + + -Basic watch queries can cause performance issues in UI frameworks like React because they return new arrays and objects on every dependent table change, even when the actual data hasn't changed. This can lead to excessive re-renders as components receive new props unnecessarily. +todo -Incremental watch queries address this problem by handling result sets in smarter ways - comparing result sets to detect actual changes, only emitting updates when data has actually changed, preserving object references when data is unchanged, and providing granular update information to minimize unnecessary re-renders. + + +todo + + + + +todo + + + + +# JavaScript SDKs: Incremental Watch Queries + +Basic watch queries can cause performance issues in UI frameworks like React because they return new arrays and objects on every dependent table change, even when the actual data hasn't changed. This can lead to excessive re-renders as components receive new props unnecessarily. + +Incremental watch queries address this problem by comparing result sets using configurable comparators and only emitting updates when the comparison detects actual data changes. Differential watch queries additionally preserve object references for unchanged items and provide detailed diff information (added/removed/updated items) to enable granular UI updates. **JavaScript Only**: Incremental watch queries are currently available only in the JavaScript SDKs. -### The WatchedQuery Class +## The `WatchedQuery` Class -All incremental queries use the new `WatchedQuery` class, which provides advanced features: +All incremental queries use the new `WatchedQuery` class +- The logic required for incrementally watched queries requires additional computation and introduces additional complexity to the implementation. For these reasons a new concept of a `WatchedQuery` class is introduced, along with a new `query` method allows building a instances of `WatchedQuery`s via `watch` and `differentialWatch` methods. +- New `updateSettings` API, see below. +Provides advanced features: - Automatically reprocesses itself if the PowerSync schema has been updated with `updateSchema`. - Automatically closes itself when the PowerSync client has been closed. - Allows for the query parameters to be updated after instantiation. - Allows shared listening to state changes. +## 1. Comparison Watch Queries +Comparison based queries behave similar to basic watched queries. These queries still query the SQLite DB under the hood on each dependant table change, but they compare the result set and only incrementally yield results if a change has been made. The latest query result is yielded as the result set. + + + -### 1. Comparison Watch Queries +Existing AsyncIterator API with configurable comparator that compares current and previous result sets, only yielding when the comparator detects changes. Use this across all SDKs to avoid re-renders from unchanged data while maintaining the familiar AsyncIterator pattern: -Comparison based queries behave similar to basic watched queries. These queries still query the SQLite DB under the hood on each dependant table change, but they compare the result set and only incrementally yield results if a change has been made. The latest query result is yielded as the result set. +```javascript +async function* pendingLists(): AsyncIterable { + for await (const result of powerSync.watch('SELECT * FROM lists WHERE state = ?', ['pending'], { + comparator: { + checkEquality: (current, previous) => JSON.stringify(current) === JSON.stringify(previous) + } + })) { + yield result.rows?._array ?? []; + } +} +``` + + + -`db.query().watch()` method +Existing Callback API with configurable comparator that compares result sets and only invokes the callback when changes are detected. Use this when you need React Native compatibility or prefer callbacks while avoiding callback execution on unchanged data: ```javascript -// Comparison-based watch query - exits early when change detected -const query = db.query({ - sql: 'SELECT * FROM todos WHERE completed = ?', - parameters: [false] +const pendingLists = (onResult: (lists: any[]) => void): void => { + powerSync.watch( + 'SELECT * FROM lists WHERE state = ?', + ['pending'], + { + onResult: (result: any) => { + onResult(result.rows?._array ?? []); + } + }, + { + comparator: { + checkEquality: (current, previous) => { + // This comparator will only report updates if the data changes. + return JSON.stringify(current) === JSON.stringify(previous); + } + } + } + ); +} +``` + + + + +WatchedQuery class that supports multiple listeners via registerListener(), automatic cleanup on PowerSync close, and updateSettings() for dynamic parameter changes. Use this as the preferred approach for JavaScript SDKs when you need query sharing, multiple subscribers, or dynamic query modification: + +```javascript +// Create an instance of a WatchedQuery +const pendingLists = db.query({ + sql: 'SELECT * FROM lists WHERE state = ?', + parameters: ['pending'] }).watch(); // The registerListener method can be used multiple times to listen for updates -const dispose = query.registerListener({ +const dispose = pendingLists.registerListener({ onData: (data) => { // This callback will be called whenever the data changes console.log('Data updated:', data); @@ -204,88 +214,113 @@ const dispose = query.registerListener({ }); ``` -### 2. Differential Watch Queries + + + +WatchedQuery class with configurable comparator that compares result sets before emitting to listeners, preventing unnecessary listener invocations when data hasn't changed. Use this when you want shared query instances plus result set comparison to minimize processing overhead: +```javascript +// Create an instance of a WatchedQuery +const pendingLists = powerSync.query({ + sql: 'SELECT * FROM lists WHERE state = ?', + parameters: ['pending'], +}).watch({ + comparator: { + checkEquality: (current, previous) => { + // This comparator will only report updates if the data changes. + return JSON.stringify(current) === JSON.stringify(previous); + } + } +}); + +// Register listeners as before... +``` + + + + +React hook that uses comparators to detect changes at the item level, only triggering re-renders when individual items are added, removed, or modified. Use this when you want built-in state management plus incremental updates for React components: + +```javascript +const { data: pendingLists, isLoading, isFetching, error } = useQuery( + 'SELECT * FROM lists WHERE state = ?', + ['pending'], + { + comparator: { + keyBy: (item) => item.id, + compareBy: (item) => JSON.stringify(item) + } + } +); +``` + + + + +React Suspense hook that preserves object references for unchanged items and uses item-level comparators to minimize re-renders. Use this when you want concurrent React features, automatic state handling, and memoization-friendly object stability: + +```javascript +const { data: lists } = useSuspenseQuery( + 'SELECT * FROM lists WHERE state = ?', + ['pending'], + { + comparator: { + keyBy: (item) => item.id, + compareBy: (item) => JSON.stringify(item) + } + } +); +``` + + + + + +- todo recommendation from Steven: Efficiently using React Memoization for the widgets. This one is a bit indirect, but the incrementally watched queries have much less benefit without this. + +## 2. Differential Watch Queries Differential queries watch a SQL query and report detailed information on the changes between result sets. This gives additional information such as the added, removed, updated rows between result set changes. -`db.query().differentialWatch()` method +Computes and reports precise diffs between result sets (added/removed/updated items) while preserving object references for unchanged items, enabling more granular UI updates. Use differential watch when you need to update only specific list items, implement optimistic UI updates, or minimize DOM manipulation by knowing exactly which items changed: ```javascript // Create an instance of a WatchedQuery -const listsQuery = db - .query({ - sql: ` - SELECT - lists.*, - COUNT(todos.id) AS total_tasks, - SUM( - CASE - WHEN todos.completed = true THEN 1 - ELSE 0 - END - ) as completed_tasks - FROM - lists - LEFT JOIN todos ON lists.id = todos.list_id - GROUP BY - lists.id; - ` - }) - .differentialWatch(); +const pendingLists = powerSync.query({ + sql: 'SELECT * FROM lists WHERE state = ?', + parameters: ['pending'] +}).differentialWatch(); // The registerListener method can be used multiple times to listen for updates -// The returned dispose function can be used to unsubscribe from the updates -const disposeSubscriber = listsQuery.registerListener({ +const dispose = pendingLists.registerListener({ onData: (data) => { // This callback will be called whenever the data changes - // The data is the result of the executor 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 - ); + 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); } }); ``` -### Advanced Features +todo recommendation from Steven: document optimisations for the differentiator methods. This depends on the table and query structure. -**Custom Differentiator Logic:** - -The incremental logic is customizable with custom diff logic: - -```javascript -const watch = db - .query({ - sql: 'SELECT * FROM todos', - mapper: (raw) => ({ - id: raw.id as string, - description: raw.description as string - }) - }) - .differentialWatch({ - differentiator: { - identify: (item) => item.id, - compareBy: (item) => JSON.stringify(item) - } - }); -``` + +- See a demo of the differential stuff in the updated YJS demo app + - Document updates are watched via a differential incremental query. New updates are passed to YJS for consolidation as they are synced. + ### Query Sharing - `WatchedQuery` instances can be shared across components: - ```javascript // Create a shared query instance const sharedTodosQuery = db @@ -302,45 +337,35 @@ const dispose2 = sharedTodosQuery.registerListener({ }); ``` -### Dynamic Parameter Updates +### React Hook for External WatchedQuery Instances -Update query parameters to affect all subscribers: +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 subscribe to the same data: ```javascript -// Updates to query parameters can be performed in a single place, affecting all subscribers -watch.updateSettings({ - query: new GetAllQuery({ sql: `SELECT * FROM todos OFFSET ? LIMIT 100`, parameters: [newOffset] }) -}); -``` - -### React Integration - -Use with React hooks: - -```javascript -import { useQuery } from '@powersync/react'; - -function TodosList() { - const { data: todos, isLoading, error } = useQuery( - 'SELECT * FROM todos WHERE completed = ?', - [false] - ); - - if (isLoading) return
Loading...
; - if (error) return
Error: {error.message}
; +// 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 component +export const MyComponent = () => { + // In React one could import the `pendingLists` query or create a context provider for various queries + const { data } = useWatchedQuerySubscription(pendingLists); + return (
- {todos.map(todo => ( - - ))} + {data.map(item =>
{item.name}
)}
); } ``` -### Performance - -Incremental watch queries can improve rendering performance: -- benchmarks show that incremental updates with differential queries coupled with React memoization render 60-80% faster -- incremental updates only render the newly added item widgets, while the standard query methods re-render the entire widget \ No newline at end of file +### Dynamic Parameter Updates +Update query parameters to affect all subscribers: +```javascript +// Updates to query parameters can be performed in a single place, affecting all subscribers +watch.updateSettings({ + query: new GetAllQuery({ sql: `SELECT * FROM todos OFFSET ? LIMIT 100`, parameters: [newOffset] }) +}); +``` \ No newline at end of file From 532ca20eafa4187ea11f82de79bc0b4ea1befd56 Mon Sep 17 00:00:00 2001 From: benitav Date: Wed, 23 Jul 2025 13:21:05 +0200 Subject: [PATCH 07/13] restructure part 2 --- usage/use-case-examples/watch-queries.mdx | 166 ++++++++++++++++------ 1 file changed, 121 insertions(+), 45 deletions(-) diff --git a/usage/use-case-examples/watch-queries.mdx b/usage/use-case-examples/watch-queries.mdx index 2caf880c..f140e447 100644 --- a/usage/use-case-examples/watch-queries.mdx +++ b/usage/use-case-examples/watch-queries.mdx @@ -1,6 +1,6 @@ --- title: "Live Queries / Watch Queries" -description: "Subscribe to real-time data changes with reactive live queries" +description: "Subscribe to real-time data changes with reactive watch queries" --- Live queries, also known as watch queries, are essential for building reactive apps where the UI automatically updates when the underlying data changes. PowerSync's watch functionality allows you to subscribe to SQL query results and receive updates whenever the dependent tables are modified. @@ -12,7 +12,7 @@ Live queries, also known as watch queries, are essential for building reactive a PowerSync provides multiple approaches to watching queries, each designed for different use cases and performance requirements: 1. **Basic Watch Queries** - These queries work across all SDKs, providing real-time updates when dependent tables change -2. **Comparison Watch Queries** - Only emit updates when data actually changes, preventing unnecessary re-renders +2. **Incremental Watch Queries** - Only emit updates when data actually changes, preventing unnecessary re-renders 3. **Differential Watch Queries** - Provide detailed information about what specifically changed between result sets Choose the approach that best fits your platform and performance needs. @@ -95,50 +95,99 @@ const { data: pendingLists, isLoading, isFetching, error } = useQuery( -todo +```dart +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()); + } + }, +) +``` -todo +```kotlin +fun watchPendingLists(): Flow> = + db.watch( + "SELECT * FROM lists WHERE state = ?", + listOf("pending"), + ) { cursor -> + ListItem( + id = cursor.getString("id"), + name = cursor.getString("name"), + ) + } +``` -todo +```swift +func watchPendingLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { + do { + for try await result in 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") + ) + } { + callback(result) + } + } catch { + print("Error in watch: \(error)") + } +} +``` -todo +```csharp +await db.Watch("SELECT * FROM lists WHERE state = ?", new[] { "pending" }, new WatchHandler +{ + OnResult = (results) => + { + Console.WriteLine("Pending Lists: "); + foreach (var result in results) + { + Console.WriteLine($"{result.id}: {result.name}"); + } + }, + OnError = (error) => + { + Console.WriteLine("Error: " + error.Message); + } +}); +``` -# JavaScript SDKs: Incremental Watch Queries -Basic watch queries can cause performance issues in UI frameworks like React because they return new arrays and objects on every dependent table change, even when the actual data hasn't changed. This can lead to excessive re-renders as components receive new props unnecessarily. +# Incremental Watch Queries -Incremental watch queries address this problem by comparing result sets using configurable comparators and only emitting updates when the comparison detects actual data changes. Differential watch queries additionally preserve object references for unchanged items and provide detailed diff information (added/removed/updated items) to enable granular UI updates. +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 hasn't changed. This can lead to excessive re-renders as components receive new props unnecessarily. - -**JavaScript Only**: Incremental watch queries are currently available only in the JavaScript SDKs. - +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 DB under the hood on each dependent table change, but compare the result set and only yield results if a change has been made. -## The `WatchedQuery` Class +Syntax: -All incremental queries use the new `WatchedQuery` class -- The logic required for incrementally watched queries requires additional computation and introduces additional complexity to the implementation. For these reasons a new concept of a `WatchedQuery` class is introduced, along with a new `query` method allows building a instances of `WatchedQuery`s via `watch` and `differentialWatch` methods. -- New `updateSettings` API, see below. - -Provides advanced features: -- Automatically reprocesses itself if the PowerSync schema has been updated with `updateSchema`. -- Automatically closes itself when the PowerSync client has been closed. -- Allows for the query parameters to be updated after instantiation. -- Allows shared listening to state changes. +```javascript +db.watch('SELECT * FROM lists WHERE state = ?', ['pending'], { comparator: { ... } }) +``` -## 1. Comparison Watch Queries -Comparison based queries behave similar to basic watched queries. These queries still query the SQLite DB under the hood on each dependant table change, but they compare the result set and only incrementally yield results if a change has been made. The latest query result is yielded as the result set. + +**JavaScript Only**: Incremental and differential watch queries are currently available only in the JavaScript SDKs. + @@ -147,7 +196,7 @@ Existing AsyncIterator API with configurable comparator that compares current an ```javascript async function* pendingLists(): AsyncIterable { - for await (const result of powerSync.watch('SELECT * FROM lists WHERE state = ?', ['pending'], { + for await (const result of db.watch('SELECT * FROM lists WHERE state = ?', ['pending'], { comparator: { checkEquality: (current, previous) => JSON.stringify(current) === JSON.stringify(previous) } @@ -164,7 +213,7 @@ Existing Callback API with configurable comparator that compares result sets and ```javascript const pendingLists = (onResult: (lists: any[]) => void): void => { - powerSync.watch( + db.watch( 'SELECT * FROM lists WHERE state = ?', ['pending'], { @@ -221,7 +270,7 @@ WatchedQuery class with configurable comparator that compares result sets before ```javascript // Create an instance of a WatchedQuery -const pendingLists = powerSync.query({ +const pendingLists = db.query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'], }).watch({ @@ -278,14 +327,21 @@ const { data: lists } = useSuspenseQuery( - todo recommendation from Steven: Efficiently using React Memoization for the widgets. This one is a bit indirect, but the incrementally watched queries have much less benefit without this. -## 2. Differential Watch Queries -Differential queries watch a SQL query and report detailed information on the changes between result sets. This gives additional information such as the added, removed, updated rows between result set changes. +# Differential Watch Queries + +Differential 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. + +Syntax: -Computes and reports precise diffs between result sets (added/removed/updated items) while preserving object references for unchanged items, enabling more granular UI updates. Use differential watch when you need to update only specific list items, implement optimistic UI updates, or minimize DOM manipulation by knowing exactly which items changed: +```javascript +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: ```javascript // Create an instance of a WatchedQuery -const pendingLists = powerSync.query({ +const pendingLists = db.query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }).differentialWatch(); @@ -314,13 +370,30 @@ const dispose = pendingLists.registerListener({ todo recommendation from Steven: document optimisations for the differentiator methods. This depends on the table and query structure. - -- See a demo of the differential stuff in the updated YJS demo app - - Document updates are watched via a differential incremental query. New updates are passed to YJS for consolidation as they are synced. - + +The [Yjs Document Collaboration Demo app](https://github.com/powersync-ja/powersync-js/tree/main/demos/yjs-react-supabase-text-collab) showcases the use of differential watch queries. New document updates are passed to Yjs for consolidation as they are synced. +- todo: link to main relevant code in app or explain key bits briefly. + + +# The `WatchedQuery` Class + +Both incremental and differential queries use the new `WatchedQuery` class. This class, along with a new `query` method allows building instances of `WatchedQuery`s via the `watch` and `differentialWatch` methods: + +```javascript +const watchedQuery = db.query({ sql: 'SELECT * FROM lists', parameters: [] }).watch() +``` + +This class provides advanced features: +- Automatically reprocesses itself if the PowerSync schema has been updated with `updateSchema`. +- Automatically closes itself when the PowerSync client has been closed. +- Allows for the query parameters to be updated after instantiation. +- Allows shared listening to state changes. +- New `updateSettings` API for dynamic parameter updates (see below). + +## Query Sharing -### Query Sharing `WatchedQuery` instances can be shared across components: + ```javascript // Create a shared query instance const sharedTodosQuery = db @@ -337,7 +410,18 @@ const dispose2 = sharedTodosQuery.registerListener({ }); ``` -### React Hook for External WatchedQuery Instances +## Dynamic Parameter Updates + +Update query parameters to affect all subscribers of the query: + +```javascript +// Updates to query parameters can be performed in a single place, affecting all subscribers +watch.updateSettings({ + query: new GetAllQuery({ sql: `SELECT * FROM todos OFFSET ? LIMIT 100`, parameters: [newOffset] }) +}); +``` + +## React Hook for External WatchedQuery Instances 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 subscribe to the same data: @@ -361,11 +445,3 @@ export const MyComponent = () => { } ``` -### Dynamic Parameter Updates -Update query parameters to affect all subscribers: -```javascript -// Updates to query parameters can be performed in a single place, affecting all subscribers -watch.updateSettings({ - query: new GetAllQuery({ sql: `SELECT * FROM todos OFFSET ? LIMIT 100`, parameters: [newOffset] }) -}); -``` \ No newline at end of file From 0047888213ca11dd58a462222645863b40c71b6a Mon Sep 17 00:00:00 2001 From: benitav Date: Wed, 23 Jul 2025 14:24:15 +0200 Subject: [PATCH 08/13] Use snippets to make watch query examples consistent --- client-sdk-references/dotnet.mdx | 17 +-- client-sdk-references/flutter.mdx | 32 +---- .../flutter/usage-examples.mdx | 17 +-- client-sdk-references/javascript-web.mdx | 27 ++--- .../javascript-spa-frameworks.mdx | 8 ++ .../javascript-web/usage-examples.mdx | 39 ++---- .../kotlin-multiplatform.mdx | 17 +-- .../kotlin-multiplatform/usage-examples.mdx | 16 +-- client-sdk-references/node.mdx | 31 +++-- .../react-native-and-expo.mdx | 41 ++----- .../react-native-and-expo/usage-examples.mdx | 39 ++---- client-sdk-references/swift.mdx | 25 +--- .../swift/usage-examples.mdx | 26 +--- .../basic-watch-query-javascript-async.mdx | 9 ++ .../basic-watch-query-javascript-callback.mdx | 12 ++ snippets/dotnet/basic-watch-query.mdx | 17 +++ snippets/flutter/basic-watch-query.mdx | 13 ++ .../basic-watch-query.mdx | 12 ++ snippets/swift/basic-watch-query.mdx | 18 +++ usage/use-case-examples/watch-queries.mdx | 113 ++++-------------- 20 files changed, 198 insertions(+), 331 deletions(-) create mode 100644 snippets/basic-watch-query-javascript-async.mdx create mode 100644 snippets/basic-watch-query-javascript-callback.mdx create mode 100644 snippets/dotnet/basic-watch-query.mdx create mode 100644 snippets/flutter/basic-watch-query.mdx create mode 100644 snippets/kotlin-multiplatform/basic-watch-query.mdx create mode 100644 snippets/swift/basic-watch-query.mdx diff --git a/client-sdk-references/dotnet.mdx b/client-sdk-references/dotnet.mdx index 7dde5e43..21204bef 100644 --- a/client-sdk-references/dotnet.mdx +++ b/client-sdk-references/dotnet.mdx @@ -5,6 +5,7 @@ sidebarTitle: Overview --- import DotNetInstallation from '/snippets/dotnet/installation.mdx'; +import DotNetWatch from '/snippets/dotnet/basic-watch-query.mdx'; @@ -276,21 +277,7 @@ Console.WriteLine(await db.Get("SELECT powersync_rs_version();")); Console.WriteLine(await db.GetAll("SELECT * FROM lists;")); // Use db.Watch() to watch queries for changes (await is used to wait for initialization): -await db.Watch("select * from lists", null, new WatchHandler -{ - OnResult = (results) => - { - Console.WriteLine("Results: "); - foreach (var result in results) - { - Console.WriteLine(result.id + ":" + result.name); - } - }, - OnError = (error) => - { - Console.WriteLine("Error: " + error.Message); - } -}); + // And db.Execute for inserts, updates and deletes: await db.Execute( diff --git a/client-sdk-references/flutter.mdx b/client-sdk-references/flutter.mdx index c1747745..433a88ac 100644 --- a/client-sdk-references/flutter.mdx +++ b/client-sdk-references/flutter.mdx @@ -6,6 +6,7 @@ sidebarTitle: Overview import SdkFeatures from '/snippets/sdk-features.mdx'; import FlutterInstallation from '/snippets/flutter/installation.mdx'; +import FlutterWatch from '/snippets/flutter/basic-watch-query.mdx'; @@ -327,36 +328,7 @@ Future> getLists() async { The [watch](https://pub.dev/documentation/powersync/latest/sqlite_async/SqliteQueries/watch.html) method executes a read query whenever a change to a dependent table is made. -```dart lib/widgets/todos_widget.dart {13-17} -import 'package:flutter/material.dart'; -import '../main.dart'; -import '../models/todolist.dart'; - -// Example Todos widget -class TodosWidget extends StatelessWidget { - const TodosWidget({super.key}); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - // You can watch any SQL query - stream: db - .watch('SELECT * FROM lists ORDER BY created_at, id') - .map((results) { - return results.map(TodoList.fromRow).toList(growable: false); - }), - builder: (context, snapshot) { - if (snapshot.hasData) { - // TODO: implement your own UI here based on the result set - return ...; - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - ); - } -} -``` + ### Mutations (PowerSync.execute) diff --git a/client-sdk-references/flutter/usage-examples.mdx b/client-sdk-references/flutter/usage-examples.mdx index 3b60e0bb..33c610e9 100644 --- a/client-sdk-references/flutter/usage-examples.mdx +++ b/client-sdk-references/flutter/usage-examples.mdx @@ -3,6 +3,8 @@ title: "Usage Examples" description: "Code snippets and guidelines for common scenarios" --- +import FlutterWatch from '/snippets/flutter/basic-watch-query.mdx'; + ## Using transactions to group changes Read and write transactions present a context where multiple changes can be made then finally committed to the DB or rolled back. This ensures that either all the changes get persisted, or no change is made to the DB (in the case of a rollback or exception). @@ -26,20 +28,7 @@ Also see [readTransaction(callback)](https://pub.dev/documentation/powersync/lat Use [watch](https://pub.dev/documentation/powersync/latest/sqlite_async/SqliteQueries/watch.html) to watch for changes to the dependent tables of any SQL query. -```dart -StreamBuilder( - // You can watch any SQL query - stream: db.watch('SELECT * FROM customers order by id asc'), - builder: (context, snapshot) { - if (snapshot.hasData) { - // TODO: implement your own UI here based on the result set - return ...; - } else { - return const Center(child: CircularProgressIndicator()); - } - }, -) -``` + ## Insert, update, and delete data in the local database diff --git a/client-sdk-references/javascript-web.mdx b/client-sdk-references/javascript-web.mdx index e10f9568..50358f03 100644 --- a/client-sdk-references/javascript-web.mdx +++ b/client-sdk-references/javascript-web.mdx @@ -6,6 +6,8 @@ sidebarTitle: "Overview" import SdkFeatures from '/snippets/sdk-features.mdx'; import JavaScriptWebInstallation from '/snippets/javascript-web/installation.mdx'; +import JavaScriptAsyncWatch from '/snippets/basic-watch-query-javascript-async.mdx'; +import JavaScriptCallbackWatch from '/snippets/basic-watch-query-javascript-callback.mdx'; @@ -219,21 +221,16 @@ export const getLists = async () => { The [watch](https://powersync-ja.github.io/powersync-js/web-sdk/classes/PowerSyncDatabase#watch) method executes a read query whenever a change to a dependent table is made. -```js -// Watch changes to lists -const abortController = new AbortController(); - -export const function watchLists = (onUpdate) => { - for await (const update of PowerSync.watch( - 'SELECT * from lists', - [], - { signal: abortController.signal } - ) - ) { - onUpdate(update); - } -} -``` + + + + + + + + + +For advanced watch query features like incremental updates and differential results, see [Live Queries / Watch Queries](/usage/use-case-examples/watch-queries). ### Mutations (PowerSync.execute, PowerSync.writeTransaction) diff --git a/client-sdk-references/javascript-web/javascript-spa-frameworks.mdx b/client-sdk-references/javascript-web/javascript-spa-frameworks.mdx index 23e2e8c1..f16b2e25 100644 --- a/client-sdk-references/javascript-web/javascript-spa-frameworks.mdx +++ b/client-sdk-references/javascript-web/javascript-spa-frameworks.mdx @@ -37,6 +37,10 @@ The main hooks available are: * `useSuspenseQuery`: This hook also allows you to access the results of a watched query, but its loading and fetching states are handled through [Suspense](https://react.dev/reference/react/Suspense). It automatically converts certain loading/fetching states into Suspense signals, triggering Suspense boundaries in parent components. + +For advanced watch query features like incremental updates and differential results for React Hooks, see [Live Queries / Watch Queries](/usage/use-case-examples/watch-queries). + + The full API Reference and example code can be found here: @@ -93,6 +97,10 @@ The main hooks available are: * `useStatus`: Access the PowerSync connectivity status. This can be used to update the UI based on whether the client is connected or not. + +For advanced watch query features like incremental updates and differential results for Vue Hooks, see [Live Queries / Watch Queries](/usage/use-case-examples/watch-queries). + + The full API Reference and example code can be found here: \ No newline at end of file diff --git a/client-sdk-references/javascript-web/usage-examples.mdx b/client-sdk-references/javascript-web/usage-examples.mdx index d52e802f..8b954406 100644 --- a/client-sdk-references/javascript-web/usage-examples.mdx +++ b/client-sdk-references/javascript-web/usage-examples.mdx @@ -3,6 +3,9 @@ title: "Usage Examples" description: "Code snippets and guidelines for common scenarios" --- +import JavaScriptAsyncWatch from '/snippets/basic-watch-query-javascript-async.mdx'; +import JavaScriptCallbackWatch from '/snippets/basic-watch-query-javascript-callback.mdx'; + ## Multiple Tab Support @@ -108,34 +111,16 @@ Also see [PowerSyncDatabase.readTransaction(callback)](https://powersync-ja.gith Use [PowerSyncDatabase.watch](https://powersync-ja.github.io/powersync-js/web-sdk/classes/PowerSyncDatabase#watch) to watch for changes in source tables. -The `watch` method can be used with a `AsyncIterable` signature as follows: - -```js -async *attachmentIds(): AsyncIterable { - for await (const result of this.powersync.watch( - `SELECT photo_id as id FROM ${TODO_TABLE} WHERE photo_id IS NOT NULL`, - [] - )) { - yield result.rows?._array.map((r) => r.id) ?? []; - } -} -``` - -As of version **1.3.3** of the SDK, the `watch` method can also be used with a callback: + + + + + + + + -```js -attachmentIds(onResult: (ids: string[]) => void): void { - this.powersync.watch( - `SELECT photo_id as id FROM ${TODO_TABLE} WHERE photo_id IS NOT NULL`, - [], - { - onResult: (result) => { - onResult(result.rows?._array.map((r) => r.id) ?? []); - } - } - ); -} -``` +For advanced watch query features like incremental updates and differential results, see [Live Queries / Watch Queries](/usage/use-case-examples/watch-queries). ## Insert, update, and delete data in the local database diff --git a/client-sdk-references/kotlin-multiplatform.mdx b/client-sdk-references/kotlin-multiplatform.mdx index d6a7beed..5d71c8de 100644 --- a/client-sdk-references/kotlin-multiplatform.mdx +++ b/client-sdk-references/kotlin-multiplatform.mdx @@ -5,6 +5,7 @@ sidebarTitle: Overview import SdkFeatures from '/snippets/sdk-features.mdx'; import KotlinMultiplatformInstallation from '/snippets/kotlin-multiplatform/installation.mdx'; +import KotlinWatch from '/snippets/kotlin-multiplatform/basic-watch-query.mdx'; @@ -242,21 +243,7 @@ suspend fun getLists(): List { The `watch` method executes a read query whenever a change to a dependent table is made. -```kotlin -// You can watch any SQL query -fun watchCustomers(): Flow> { - // TODO: implement your UI based on the result set - return database.watch( - "SELECT * FROM customers" - ) { cursor -> - User( - id = cursor.getString("id"), - name = cursor.getString("name"), - email = cursor.getString("email") - ) - } -} -``` + ### Mutations (PowerSync.execute) diff --git a/client-sdk-references/kotlin-multiplatform/usage-examples.mdx b/client-sdk-references/kotlin-multiplatform/usage-examples.mdx index 928ece5f..c733859b 100644 --- a/client-sdk-references/kotlin-multiplatform/usage-examples.mdx +++ b/client-sdk-references/kotlin-multiplatform/usage-examples.mdx @@ -3,6 +3,8 @@ title: "Usage Examples" description: "Code snippets and guidelines for common scenarios" --- +import KotlinWatch from '/snippets/kotlin-multiplatform/basic-watch-query.mdx'; + ## Using transactions to group changes Use `writeTransaction` to group statements that can write to the database. @@ -24,19 +26,7 @@ database.writeTransaction { Use the `watch` method to watch for changes to the dependent tables of any SQL query. -```kotlin -// You can watch any SQL query -fun watchCustomers(): Flow> { - // TODO: implement your UI based on the result set - return database.watch("SELECT * FROM customers", mapper = { cursor -> - User( - id = cursor.getString("id"), - name = cursor.getString("name"), - email = cursor.getString("email") - ) - }) -} -``` + ## Insert, update, and delete data in the local database diff --git a/client-sdk-references/node.mdx b/client-sdk-references/node.mdx index eab80810..07762799 100644 --- a/client-sdk-references/node.mdx +++ b/client-sdk-references/node.mdx @@ -6,6 +6,8 @@ sidebarTitle: Overview import SdkFeatures from '/snippets/sdk-features.mdx'; import NodeInstallation from '/snippets/node/installation.mdx'; +import JavaScriptAsyncWatch from '/snippets/basic-watch-query-javascript-async.mdx'; +import JavaScriptCallbackWatch from '/snippets/basic-watch-query-javascript-callback.mdx'; This page describes the PowerSync _client_ SDK for Node.js. @@ -147,7 +149,7 @@ await db.waitForFirstSync(); // Optional, to wait for a complete snapshot of dat ## Usage After connecting the client database, it is ready to be used. The API to run queries and updates is identical to our -[web SDK](/client-sdk-references/javascript-web#using-powersync%3A-crud-functions): +[JavaScript/Web SDK](/client-sdk-references/javascript-web#using-powersync%3A-crud-functions): ```js // Use db.get() to fetch a single row: @@ -156,14 +158,6 @@ console.log(await db.get('SELECT powersync_rs_version();')); // Or db.getAll() to fetch all: console.log(await db.getAll('SELECT * FROM lists;')); -// Use db.watch() to watch queries for changes: -const watchLists = async () => { - for await (const rows of db.watch('SELECT * FROM lists;')) { - console.log('Has todo lists', rows.rows!._array); - } -}; -watchLists(); - // And db.execute for inserts, updates and deletes: await db.execute( "INSERT INTO lists (id, created_at, name, owner_id) VALUEs (uuid(), datetime('now'), ?, uuid());", @@ -171,8 +165,23 @@ await db.execute( ); ``` -PowerSync runs queries asynchronously on a background pool of workers and automatically configures WAL to -allow a writer and multiple readers to operate in parallel. +### Watch Queries + +The `db.watch()` method executes a read query whenever a change to a dependent table is made. + + + + + + + + + + +For advanced watch query features like incremental updates and differential results, see [Live Queries / Watch Queries](/usage/use-case-examples/watch-queries). + + +PowerSync runs queries asynchronously on a background pool of workers and automatically configures WAL to allow a writer and multiple readers to operate in parallel. ## Configure Logging diff --git a/client-sdk-references/react-native-and-expo.mdx b/client-sdk-references/react-native-and-expo.mdx index b8f62e7e..4f208c5c 100644 --- a/client-sdk-references/react-native-and-expo.mdx +++ b/client-sdk-references/react-native-and-expo.mdx @@ -6,6 +6,8 @@ sidebarTitle: "Overview" import SdkFeatures from '/snippets/sdk-features.mdx'; import ReactNativeInstallation from '/snippets/react-native/installation.mdx'; +import JavaScriptAsyncWatch from '/snippets/basic-watch-query-javascript-async.mdx'; +import JavaScriptCallbackWatch from '/snippets/basic-watch-query-javascript-callback.mdx'; @@ -309,37 +311,16 @@ export const ListsWidget = () => { The [watch](https://powersync-ja.github.io/powersync-js/react-native-sdk/classes/PowerSyncDatabase#watch) method executes a read query whenever a change to a dependent table is made. It can be used with an `AsyncGenerator`, or with a callback. -```js ListsWidget.jsx -import { FlatList, Text } from 'react-native'; -import { powersync } from "../powersync/system"; - -export const ListsWidget = () => { - const [lists, setLists] = React.useState([]); - - React.useEffect(() => { - const abortController = new AbortController(); - - // Option 1: Use with AsyncGenerator - (async () => { - for await(const update of powersync.watch('SELECT * from lists', [], {signal: abortController.signal})) { - setLists(update) - } - })(); - - // Option 2: Use a callback (available since version 1.3.3 of the SDK) - powersync.watch('SELECT * from lists', [], { onResult: (result) => setLists(result) }, { signal: abortController.signal }); - - return () => { - abortController.abort(); - } - }, []); + + + + + + + + - return ( ({ key: list.id, ...list }))} - renderItem={({ item }) => {item.name}} - />) -} -``` +For advanced watch query features like incremental updates and differential results, see [Live Queries / Watch Queries](/usage/use-case-examples/watch-queries). ### Mutations (PowerSync.execute) diff --git a/client-sdk-references/react-native-and-expo/usage-examples.mdx b/client-sdk-references/react-native-and-expo/usage-examples.mdx index 88fc7c10..68191af2 100644 --- a/client-sdk-references/react-native-and-expo/usage-examples.mdx +++ b/client-sdk-references/react-native-and-expo/usage-examples.mdx @@ -3,6 +3,9 @@ title: "Usage Examples" description: "Code snippets and guidelines for common scenarios" --- +import JavaScriptAsyncWatch from '/snippets/basic-watch-query-javascript-async.mdx'; +import JavaScriptCallbackWatch from '/snippets/basic-watch-query-javascript-callback.mdx'; + ## Using Hooks A separate `powersync-react` package is available containing React hooks for PowerSync: @@ -83,34 +86,16 @@ Also see [PowerSyncDatabase.readTransaction(callback)](https://powersync-ja.gith Use [PowerSyncDatabase.watch](https://powersync-ja.github.io/powersync-js/react-native-sdk/classes/PowerSyncDatabase#watch) to watch for changes in source tables. -The `watch` method can be used with a `AsyncIterable` signature as follows: - -```js -async *attachmentIds(): AsyncIterable { - for await (const result of this.powersync.watch( - `SELECT photo_id as id FROM ${TODO_TABLE} WHERE photo_id IS NOT NULL`, - [] - )) { - yield result.rows?._array.map((r) => r.id) ?? []; - } -} -``` - -As of version **1.3.3** of the SDK, the `watch` method can also be used with a callback: + + + + + + + + -```js -attachmentIds(onResult: (ids: string[]) => void): void { - this.powersync.watch( - `SELECT photo_id as id FROM ${TODO_TABLE} WHERE photo_id IS NOT NULL`, - [], - { - onResult: (result) => { - onResult(result.rows?._array.map((r) => r.id) ?? []); - } - } - ); -} -``` +For advanced watch query features like incremental updates and differential results, see [Live Queries / Watch Queries](/usage/use-case-examples/watch-queries). ## Insert, update, and delete data in the local database diff --git a/client-sdk-references/swift.mdx b/client-sdk-references/swift.mdx index 1a868971..44a45ae7 100644 --- a/client-sdk-references/swift.mdx +++ b/client-sdk-references/swift.mdx @@ -5,6 +5,7 @@ sidebarTitle: "Overview" import SdkFeatures from '/snippets/sdk-features.mdx'; import SwiftInstallation from '/snippets/swift/installation.mdx'; +import SwiftWatch from '/snippets/swift/basic-watch-query.mdx'; @@ -222,29 +223,7 @@ func getLists() async throws { The `watch` method executes a read query whenever a change to a dependent table is made. -```swift -// You can watch any SQL query -func watchLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { - do { - for try await lists in try self.db.watch( - sql: "SELECT * FROM \(LISTS_TABLE)", - parameters: [], - mapper: { cursor in - try ListContent( - id: cursor.getString(name: "id"), - name: cursor.getString(name: "name"), - createdAt: cursor.getString(name: "created_at"), - ownerId: cursor.getString(name: "owner_id") - ) - } - ) { - callback(lists) - } - } catch { - print("Error in watch: \(error)") - } -} -``` + ### Mutations (PowerSync.execute) diff --git a/client-sdk-references/swift/usage-examples.mdx b/client-sdk-references/swift/usage-examples.mdx index 168df7a9..3a494ee8 100644 --- a/client-sdk-references/swift/usage-examples.mdx +++ b/client-sdk-references/swift/usage-examples.mdx @@ -3,6 +3,8 @@ title: "Usage Examples" description: "Code snippets and guidelines for common scenarios in Swift" --- +import SwiftWatch from '/snippets/swift/basic-watch-query.mdx'; + ## Using transactions to group changes Read and write transactions present a context where multiple changes can be made then finally committed to the DB or rolled back. This ensures that either all the changes get persisted, or no change is made to the DB (in the case of a rollback or exception). @@ -23,29 +25,7 @@ Also see [`readTransaction`](https://powersync-ja.github.io/powersync-swift/docu Use `watch` to watch for changes to the dependent tables of any SQL query. -```swift -// Watch for changes to the lists table -func watchLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { - do { - for try await lists in try self.db.watch( - sql: "SELECT * FROM \(LISTS_TABLE)", - parameters: [], - mapper: { cursor in - try ListContent( - id: cursor.getString(name: "id"), - name: cursor.getString(name: "name"), - createdAt: cursor.getString(name: "created_at"), - ownerId: cursor.getString(name: "owner_id") - ) - } - ) { - callback(lists) - } - } catch { - print("Error in watch: \(error)") - } -} -``` + ## Insert, update, and delete data in the local database diff --git a/snippets/basic-watch-query-javascript-async.mdx b/snippets/basic-watch-query-javascript-async.mdx new file mode 100644 index 00000000..2c464ede --- /dev/null +++ b/snippets/basic-watch-query-javascript-async.mdx @@ -0,0 +1,9 @@ +```javascript +async function* pendingLists(): AsyncIterable { + for await (const result of db.watch( + `SELECT * FROM lists WHERE state = ?`, + ['pending'] + )) { + yield result.rows?._array ?? []; + } +} \ No newline at end of file diff --git a/snippets/basic-watch-query-javascript-callback.mdx b/snippets/basic-watch-query-javascript-callback.mdx new file mode 100644 index 00000000..0c373dcb --- /dev/null +++ b/snippets/basic-watch-query-javascript-callback.mdx @@ -0,0 +1,12 @@ +```javascript +const pendingLists = (onResult: (lists: any[]) => void): void => { + db.watch( + 'SELECT * FROM lists WHERE state = ?', + ['pending'], + { + onResult: (result: any) => { + onResult(result.rows?._array ?? []); + } + } + ); +} \ No newline at end of file diff --git a/snippets/dotnet/basic-watch-query.mdx b/snippets/dotnet/basic-watch-query.mdx new file mode 100644 index 00000000..dc52b99c --- /dev/null +++ b/snippets/dotnet/basic-watch-query.mdx @@ -0,0 +1,17 @@ +```csharp +await db.Watch("SELECT * FROM lists WHERE state = ?", new[] { "pending" }, new WatchHandler +{ + OnResult = (results) => + { + Console.WriteLine("Pending Lists: "); + foreach (var result in results) + { + Console.WriteLine($"{result.id}: {result.name}"); + } + }, + OnError = (error) => + { + Console.WriteLine("Error: " + error.Message); + } +}); +``` \ No newline at end of file diff --git a/snippets/flutter/basic-watch-query.mdx b/snippets/flutter/basic-watch-query.mdx new file mode 100644 index 00000000..56a6b64f --- /dev/null +++ b/snippets/flutter/basic-watch-query.mdx @@ -0,0 +1,13 @@ +```dart +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()); + } + }, +) +``` \ No newline at end of file diff --git a/snippets/kotlin-multiplatform/basic-watch-query.mdx b/snippets/kotlin-multiplatform/basic-watch-query.mdx new file mode 100644 index 00000000..02a6ed62 --- /dev/null +++ b/snippets/kotlin-multiplatform/basic-watch-query.mdx @@ -0,0 +1,12 @@ +```kotlin +fun watchPendingLists(): Flow> = + db.watch( + "SELECT * FROM lists WHERE state = ?", + listOf("pending"), + ) { cursor -> + ListItem( + id = cursor.getString("id"), + name = cursor.getString("name"), + ) + } +``` \ No newline at end of file diff --git a/snippets/swift/basic-watch-query.mdx b/snippets/swift/basic-watch-query.mdx new file mode 100644 index 00000000..0a02928c --- /dev/null +++ b/snippets/swift/basic-watch-query.mdx @@ -0,0 +1,18 @@ +```swift +func watchPendingLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { + do { + for try await result in 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") + ) + } { + callback(result) + } + } catch { + print("Error in watch: \(error)") + } +} \ No newline at end of file diff --git a/usage/use-case-examples/watch-queries.mdx b/usage/use-case-examples/watch-queries.mdx index f140e447..01a277c4 100644 --- a/usage/use-case-examples/watch-queries.mdx +++ b/usage/use-case-examples/watch-queries.mdx @@ -3,9 +3,14 @@ title: "Live Queries / Watch Queries" description: "Subscribe to real-time data changes with reactive watch queries" --- -Live queries, also known as watch queries, are essential for building reactive apps where the UI automatically updates when the underlying data changes. PowerSync's watch functionality allows you to subscribe to SQL query results and receive updates whenever the dependent tables are modified. +import JavaScriptAsyncWatch from '/snippets/basic-watch-query-javascript-async.mdx'; +import JavaScriptCallbackWatch from '/snippets/basic-watch-query-javascript-callback.mdx'; +import FlutterWatch from '/snippets/flutter/basic-watch-query.mdx'; +import KotlinWatch from '/snippets/kotlin-multiplatform/basic-watch-query.mdx'; +import SwiftWatch from '/snippets/swift/basic-watch-query.mdx'; +import DotNetWatch from '/snippets/dotnet/basic-watch-query.mdx'; -- todo: link to this page from relevant sections in SDK references +Live queries, also known as watch queries, are essential for building reactive apps where the UI automatically updates when the underlying data changes. PowerSync's watch functionality allows you to subscribe to SQL query results and receive updates whenever the dependent tables are modified. # Overview @@ -17,44 +22,25 @@ PowerSync provides multiple approaches to watching queries, each designed for di Choose the approach that best fits your platform and performance needs. -# Basic Watch Queries by Platform +# Basic Watch Queries 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 for backwards compatibility. +Scroll horizontally to find your preferred framework for an example: + The original watch method using AsyncIterator pattern. This is the foundational watch API that works across all JavaScript environments and is being maintained for backwards compatibility. -```javascript -async function* pendingLists(): AsyncIterable { - for await (const result of db.watch( - `SELECT * FROM lists WHERE state = ?`, - ['pending'] - )) { - yield result.rows?._array ?? []; - } -} -``` + -The callback-based watch method that doesn't require AsyncIterator polyfills. Use this approach when you need React Native compatibility or prefer synchronous method signatures: +The callback-based watch method that doesn't require AsyncIterator polyfills. Use this approach when you need smoother React Native compatibility or prefer synchronous method signatures: -```javascript -const pendingLists = (onResult: (lists: any[]) => void): void => { - db.watch( - 'SELECT * FROM lists WHERE state = ?', - ['pending'], - { - onResult: (result: any) => { - onResult(result.rows?._array ?? []); - } - } - ); -} -``` + @@ -95,79 +81,30 @@ const { data: pendingLists, isLoading, isFetching, error } = useQuery( -```dart -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: + + -```kotlin -fun watchPendingLists(): Flow> = - 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: + + -```swift -func watchPendingLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { - do { - for try await result in 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") - ) - } { - callback(result) - } - } catch { - print("Error in watch: \(error)") - } -} -``` +Use this method to watch for changes to the dependent tables of any SQL query: + + -```csharp -await db.Watch("SELECT * FROM lists WHERE state = ?", new[] { "pending" }, new WatchHandler -{ - OnResult = (results) => - { - Console.WriteLine("Pending Lists: "); - foreach (var result in results) - { - Console.WriteLine($"{result.id}: {result.name}"); - } - }, - OnError = (error) => - { - Console.WriteLine("Error: " + error.Message); - } -}); -``` +Use this method to watch for changes to the dependent tables of any SQL query: + + From 0b715140633019949c9d37477d2b1c2627124544 Mon Sep 17 00:00:00 2001 From: benitav Date: Wed, 23 Jul 2025 14:25:46 +0200 Subject: [PATCH 09/13] Update ref on mongodb page --- migration-guides/mongodb-atlas.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migration-guides/mongodb-atlas.mdx b/migration-guides/mongodb-atlas.mdx index 8c5ebe91..56f889a8 100644 --- a/migration-guides/mongodb-atlas.mdx +++ b/migration-guides/mongodb-atlas.mdx @@ -502,7 +502,7 @@ The same applies to writing data: `INSERT`, `UPDATE` and `DELETE` statements are #### Live queries -PowerSync supports "live queries" or "watch queries" which automatically refresh when data in the SQLite database is updated (e.g. as a result of syncing from the server). This allows for real-time reactivity of your app UI. See the [Client SDK documentation](/client-sdk-references/introduction) for your specific platform for more details. +PowerSync supports "live queries" or "watch queries" which automatically refresh when data in the SQLite database is updated (e.g. as a result of syncing from the server). This allows for real-time reactivity of your app UI. See the [Live Queries/Watch Queries](/usage/use-case-examples/watch-queries) page for more details. ### 8. Accept uploads on the backend From 436a7fa5a3e5b799c981b14b2f62eb7a46280daa Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Thu, 24 Jul 2025 09:06:25 +0200 Subject: [PATCH 10/13] Add note about React Memoization --- usage/use-case-examples/watch-queries.mdx | 184 +++++++++++++--------- 1 file changed, 106 insertions(+), 78 deletions(-) diff --git a/usage/use-case-examples/watch-queries.mdx b/usage/use-case-examples/watch-queries.mdx index 01a277c4..6ca863b7 100644 --- a/usage/use-case-examples/watch-queries.mdx +++ b/usage/use-case-examples/watch-queries.mdx @@ -1,6 +1,6 @@ --- -title: "Live Queries / Watch Queries" -description: "Subscribe to real-time data changes with reactive watch queries" +title: 'Live Queries / Watch Queries' +description: 'Subscribe to real-time data changes with reactive watch queries' --- import JavaScriptAsyncWatch from '/snippets/basic-watch-query-javascript-async.mdx'; @@ -31,7 +31,7 @@ Scroll horizontally to find your preferred framework for an example: -The original watch method using AsyncIterator pattern. This is the foundational watch API that works across all JavaScript environments and is being maintained for backwards compatibility. +The original watch method using AsyncIterator pattern. This is the foundational watch API that works across all JavaScript environments and is being maintained for backwards compatibility. @@ -48,10 +48,12 @@ The callback-based watch method that doesn't require AsyncIterator polyfills. Us React hook that combines watch functionality with built-in loading, fetching, and error states. Use this when you need convenient state management without React Suspense: ```javascript -const { data: pendingLists, isLoading, isFetching, error } = useQuery( - 'SELECT * FROM lists WHERE state = ?', - ['pending'] -); +const { + data: pendingLists, + isLoading, + isFetching, + error +} = useQuery('SELECT * FROM lists WHERE state = ?', ['pending']); ``` @@ -60,10 +62,7 @@ const { data: pendingLists, isLoading, isFetching, error } = useQuery( React Suspense-based hook that automatically handles loading and error states through Suspense boundaries. Use this when you want to leverage React's concurrent features and avoid manual state handling: ```javascript -const { data: pendingLists } = useSuspenseQuery( - 'SELECT * FROM lists WHERE state = ?', - ['pending'] -); +const { data: pendingLists } = useSuspenseQuery('SELECT * FROM lists WHERE state = ?', ['pending']); ``` @@ -72,10 +71,12 @@ const { data: pendingLists } = useSuspenseQuery( Vue composition API hook with built-in loading, fetching, and error states. Use this for reactive watch queries in Vue applications: ```javascript -const { data: pendingLists, isLoading, isFetching, error } = useQuery( - 'SELECT * FROM lists WHERE state = ?', - ['pending'] -); +const { + data: pendingLists, + isLoading, + isFetching, + error +} = useQuery('SELECT * FROM lists WHERE state = ?', ['pending']); ``` @@ -109,7 +110,6 @@ Use this method to watch for changes to the dependent tables of any SQL query: - # Incremental Watch Queries 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 hasn't changed. This can lead to excessive re-renders as components receive new props unnecessarily. @@ -123,7 +123,7 @@ db.watch('SELECT * FROM lists WHERE state = ?', ['pending'], { comparator: { ... ``` -**JavaScript Only**: Incremental and differential watch queries are currently available only in the JavaScript SDKs. + **JavaScript Only**: Incremental and differential watch queries are currently available only in the JavaScript SDKs. @@ -167,7 +167,7 @@ const pendingLists = (onResult: (lists: any[]) => void): void => { } } ); -} +}; ``` @@ -177,10 +177,12 @@ WatchedQuery class that supports multiple listeners via registerListener(), auto ```javascript // Create an instance of a WatchedQuery -const pendingLists = db.query({ - sql: 'SELECT * FROM lists WHERE state = ?', - parameters: ['pending'] -}).watch(); +const pendingLists = db + .query({ + sql: 'SELECT * FROM lists WHERE state = ?', + parameters: ['pending'] + }) + .watch(); // The registerListener method can be used multiple times to listen for updates const dispose = pendingLists.registerListener({ @@ -207,17 +209,19 @@ WatchedQuery class with configurable comparator that compares result sets before ```javascript // Create an instance of a WatchedQuery -const pendingLists = db.query({ - sql: 'SELECT * FROM lists WHERE state = ?', - parameters: ['pending'], -}).watch({ - comparator: { - checkEquality: (current, previous) => { - // This comparator will only report updates if the data changes. - return JSON.stringify(current) === JSON.stringify(previous); +const pendingLists = db + .query({ + sql: 'SELECT * FROM lists WHERE state = ?', + parameters: ['pending'] + }) + .watch({ + comparator: { + checkEquality: (current, previous) => { + // This comparator will only report updates if the data changes. + return JSON.stringify(current) === JSON.stringify(previous); + } } - } -}); + }); // Register listeners as before... ``` @@ -228,16 +232,17 @@ const pendingLists = db.query({ React hook that uses comparators to detect changes at the item level, only triggering re-renders when individual items are added, removed, or modified. Use this when you want built-in state management plus incremental updates for React components: ```javascript -const { data: pendingLists, isLoading, isFetching, error } = useQuery( - 'SELECT * FROM lists WHERE state = ?', - ['pending'], - { - comparator: { - keyBy: (item) => item.id, - compareBy: (item) => JSON.stringify(item) - } +const { + data: pendingLists, + isLoading, + isFetching, + error +} = useQuery('SELECT * FROM lists WHERE state = ?', ['pending'], { + comparator: { + keyBy: (item) => item.id, + compareBy: (item) => JSON.stringify(item) } -); +}); ``` @@ -246,23 +251,40 @@ const { data: pendingLists, isLoading, isFetching, error } = useQuery( React Suspense hook that preserves object references for unchanged items and uses item-level comparators to minimize re-renders. Use this when you want concurrent React features, automatic state handling, and memoization-friendly object stability: ```javascript -const { data: lists } = useSuspenseQuery( - 'SELECT * FROM lists WHERE state = ?', - ['pending'], - { - comparator: { - keyBy: (item) => item.id, - compareBy: (item) => JSON.stringify(item) - } +const { data: lists } = useSuspenseQuery('SELECT * FROM lists WHERE state = ?', ['pending'], { + comparator: { + keyBy: (item) => item.id, + compareBy: (item) => JSON.stringify(item) } -); +}); ``` - + +Providing a `comparator` 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. + +```jsx +const TodoListsWidget = () => { + const {data: lists} = useQuery('[SQL]', [...parameters], { comparator: DEFAULT_COMPARATOR}) + + return + { + // The individual row widgets will only re-render if the corresponding row has changed + lists.map(listRecord => ) + } + +} + +const TodoWidget = React.memo(({record}) => { + return + {record.name} + +}) +``` + -- todo recommendation from Steven: Efficiently using React Memoization for the widgets. This one is a bit indirect, but the incrementally watched queries have much less benefit without this. + # Differential Watch Queries @@ -271,17 +293,19 @@ Differential queries go a step further than incremental watched queries by compu Syntax: ```javascript -db.query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }).differentialWatch() +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: ```javascript // Create an instance of a WatchedQuery -const pendingLists = db.query({ - sql: 'SELECT * FROM lists WHERE state = ?', - parameters: ['pending'] -}).differentialWatch(); +const pendingLists = db + .query({ + sql: 'SELECT * FROM lists WHERE state = ?', + parameters: ['pending'] + }) + .differentialWatch(); // The registerListener method can be used multiple times to listen for updates const dispose = pendingLists.registerListener({ @@ -299,7 +323,7 @@ const dispose = pendingLists.registerListener({ console.error('Query error:', error); }, onDiff: (diff) => { - // This callback will be called whenever the data changes. + // This callback will be called whenever the data changes. console.log('Data updated:', diff.added, diff.updated); } }); @@ -308,19 +332,22 @@ const dispose = pendingLists.registerListener({ todo recommendation from Steven: document optimisations for the differentiator methods. This depends on the table and query structure. -The [Yjs Document Collaboration Demo app](https://github.com/powersync-ja/powersync-js/tree/main/demos/yjs-react-supabase-text-collab) showcases the use of differential watch queries. New document updates are passed to Yjs for consolidation as they are synced. -- todo: link to main relevant code in app or explain key bits briefly. + The [Yjs Document Collaboration Demo + app](https://github.com/powersync-ja/powersync-js/tree/main/demos/yjs-react-supabase-text-collab) showcases the use of + differential watch queries. New document updates are passed to Yjs for consolidation as they are synced. - todo: link + to main relevant code in app or explain key bits briefly. # The `WatchedQuery` Class -Both incremental and differential queries use the new `WatchedQuery` class. This class, along with a new `query` method allows building instances of `WatchedQuery`s via the `watch` and `differentialWatch` methods: +Both incremental and differential queries use the new `WatchedQuery` class. This class, along with a new `query` method allows building instances of `WatchedQuery`s via the `watch` and `differentialWatch` methods: ```javascript -const watchedQuery = db.query({ sql: 'SELECT * FROM lists', parameters: [] }).watch() +const watchedQuery = db.query({ sql: 'SELECT * FROM lists', parameters: [] }).watch(); ``` This class provides advanced features: + - Automatically reprocesses itself if the PowerSync schema has been updated with `updateSchema`. - Automatically closes itself when the PowerSync client has been closed. - Allows for the query parameters to be updated after instantiation. @@ -332,18 +359,16 @@ This class provides advanced features: `WatchedQuery` instances can be shared across components: ```javascript -// Create a shared query instance -const sharedTodosQuery = db - .query({ sql: 'SELECT * FROM todos WHERE list_id = ?', parameters: [listId] }) - .watch(); +// Create a shared query instance +const sharedTodosQuery = db.query({ sql: 'SELECT * FROM todos WHERE list_id = ?', parameters: [listId] }).watch(); // Multiple components can listen to the same query -const dispose1 = sharedTodosQuery.registerListener({ - onData: (data) => updateTodosList(data) +const dispose1 = sharedTodosQuery.registerListener({ + onData: (data) => updateTodosList(data) }); -const dispose2 = sharedTodosQuery.registerListener({ - onData: (data) => updateTodosCount(data.length) +const dispose2 = sharedTodosQuery.registerListener({ + onData: (data) => updateTodosCount(data.length) }); ``` @@ -364,21 +389,24 @@ When you need to share query instances across components or manage their lifecyc ```javascript // 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(); +const pendingLists = db + .query({ + sql: 'SELECT * FROM lists WHERE state = ?', + parameters: ['pending'] + }) + .watch(); // In the component export const MyComponent = () => { // In React one could import the `pendingLists` query or create a context provider for various queries const { data } = useWatchedQuerySubscription(pendingLists); - + return (
- {data.map(item =>
{item.name}
)} + {data.map((item) => ( +
{item.name}
+ ))}
); -} +}; ``` - From c400fd0a9bf4813b86017165f018800ab8b416ca Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Thu, 24 Jul 2025 09:09:12 +0200 Subject: [PATCH 11/13] link to yjs demo. format JSX. --- usage/use-case-examples/watch-queries.mdx | 35 +++++++++++++---------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/usage/use-case-examples/watch-queries.mdx b/usage/use-case-examples/watch-queries.mdx index 6ca863b7..567da448 100644 --- a/usage/use-case-examples/watch-queries.mdx +++ b/usage/use-case-examples/watch-queries.mdx @@ -266,22 +266,25 @@ Providing a `comparator` to the React hooks ensures that components only re-rend ```jsx const TodoListsWidget = () => { - const {data: lists} = useQuery('[SQL]', [...parameters], { comparator: DEFAULT_COMPARATOR}) + const { data: lists } = useQuery('[SQL]', [...parameters], { comparator: DEFAULT_COMPARATOR }); - return - { - // The individual row widgets will only re-render if the corresponding row has changed - lists.map(listRecord => ) - } - -} + return ( + + { + // The individual row widgets will only re-render if the corresponding row has changed + lists.map((listRecord) => ( + + )) + } + + ); +}; -const TodoWidget = React.memo(({record}) => { - return - {record.name} - -}) +const TodoWidget = React.memo(({ record }) => { + return {record.name}; +}); ``` + @@ -334,8 +337,10 @@ todo recommendation from Steven: document optimisations for the differentiator m The [Yjs Document Collaboration Demo app](https://github.com/powersync-ja/powersync-js/tree/main/demos/yjs-react-supabase-text-collab) showcases the use of - differential watch queries. New document updates are passed to Yjs for consolidation as they are synced. - todo: link - to main relevant code in app or explain key bits briefly. + differential watch queries. New document updates are passed to Yjs for consolidation as they are synced. See the + implementation + [here](https://github.com/powersync-ja/powersync-js/blob/main/demos/yjs-react-supabase-text-collab/src/library/powersync/PowerSyncYjsProvider.ts) + for more details. # The `WatchedQuery` Class From d68c82377f89edf9fcd5b7059a36f23b0993eb92 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Thu, 24 Jul 2025 09:22:16 +0200 Subject: [PATCH 12/13] Mention more efficient comparators --- usage/use-case-examples/watch-queries.mdx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/usage/use-case-examples/watch-queries.mdx b/usage/use-case-examples/watch-queries.mdx index 567da448..8c35ed50 100644 --- a/usage/use-case-examples/watch-queries.mdx +++ b/usage/use-case-examples/watch-queries.mdx @@ -332,7 +332,23 @@ const dispose = pendingLists.registerListener({ }); ``` -todo recommendation from Steven: document optimisations for the differentiator methods. This depends on the table and query structure. +By default, the `differentialWatch()` method uses a `DEFAULT_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, you can improve performance by supplying a custom comparator. Such as comparing only a specific column (e.g., `updated_at`) if it always changes when the row is updated. + +```javascript +const pendingLists = db + .query({ + sql: 'SELECT * FROM lists WHERE state = ?', + parameters: ['pending'] + }) + .differentialWatch({ + comparator: { + keyBy: (item) => item.id, + compareBy: (item) => item.updated_at // if the column is guaranteed to changed for any row mutation. + } + }); +``` The [Yjs Document Collaboration Demo From d427d95a452ed4d0f18392ccc7f132eecb1f3e37 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Thu, 24 Jul 2025 09:29:50 +0200 Subject: [PATCH 13/13] update Swift watch demo to be cleaner --- snippets/swift/basic-watch-query.mdx | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/snippets/swift/basic-watch-query.mdx b/snippets/swift/basic-watch-query.mdx index 0a02928c..112fa5ea 100644 --- a/snippets/swift/basic-watch-query.mdx +++ b/snippets/swift/basic-watch-query.mdx @@ -1,18 +1,13 @@ ```swift -func watchPendingLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { - do { - for try await result in 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") - ) - } { - callback(result) - } - } catch { - print("Error in watch: \(error)") +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"), + ) } -} \ No newline at end of file +} +```