diff --git a/client-sdk-references/dotnet.mdx b/client-sdk-references/dotnet.mdx index 7dde5e4..21204be 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 c174774..433a88a 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 3b60e0b..33c610e 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 e10f956..50358f0 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 23e2e8c..f16b2e2 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 d52e802..8b95440 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 d6a7bee..5d71c8d 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 928ece5..c733859 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 eab8081..0776279 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 b8f62e7..4f208c5 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 88fc7c1..68191af 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 1a86897..44a45ae 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 168df7a..3a494ee 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/docs.json b/docs.json index b578466..8beb273 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/migration-guides/mongodb-atlas.mdx b/migration-guides/mongodb-atlas.mdx index 8c5ebe9..56f889a 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 diff --git a/snippets/basic-watch-query-javascript-async.mdx b/snippets/basic-watch-query-javascript-async.mdx new file mode 100644 index 0000000..2c464ed --- /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 0000000..0c373dc --- /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 0000000..dc52b99 --- /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 0000000..56a6b64 --- /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 0000000..02a6ed6 --- /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 0000000..112fa5e --- /dev/null +++ b/snippets/swift/basic-watch-query.mdx @@ -0,0 +1,13 @@ +```swift +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"), + ) + } +} +``` diff --git a/usage/use-case-examples.mdx b/usage/use-case-examples.mdx index 403405f..5c513d6 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 0000000..8c35ed5 --- /dev/null +++ b/usage/use-case-examples/watch-queries.mdx @@ -0,0 +1,433 @@ +--- +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'; +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'; + +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 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. **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. + +# 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. + + + + + + +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: + + + + + + +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']); +``` + + + + +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']); +``` + + + + +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']); +``` + + + + +Use this method to watch for changes to the dependent tables of any SQL query: + + + + + + +Use this method to watch for changes to the dependent tables of any SQL query: + + + + + + +Use this method to watch for changes to the dependent tables of any SQL query: + + + + + + +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. + +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. + +Syntax: + +```javascript +db.watch('SELECT * FROM lists WHERE state = ?', ['pending'], { comparator: { ... } }) +``` + + + **JavaScript Only**: Incremental and differential watch queries are currently available only in the JavaScript SDKs. + + + + + +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: + +```javascript +async function* pendingLists(): AsyncIterable { + for await (const result of db.watch('SELECT * FROM lists WHERE state = ?', ['pending'], { + comparator: { + checkEquality: (current, previous) => JSON.stringify(current) === JSON.stringify(previous) + } + })) { + yield result.rows?._array ?? []; + } +} +``` + + + + +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 +const pendingLists = (onResult: (lists: any[]) => void): void => { + db.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 = pendingLists.registerListener({ + onData: (data) => { + // This callback will be called whenever the data changes + console.log('Data updated:', data); + }, + onStateChange: (state) => { + // This callback will be called whenever the state changes + // The state contains metadata about the query, such as isFetching, isLoading, etc. + console.log('State changed:', state.error, state.isFetching, state.isLoading, state.data); + }, + onError: (error) => { + // This callback will be called if the query fails + console.error('Query error:', error); + } +}); +``` + + + + +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 = 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... +``` + + + + +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) + } +}); +``` + + + + +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}; +}); +``` + + + + + +# 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: + +```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 = 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({ + onData: (data) => { + // This callback will be called whenever the data changes + console.log('Data updated:', data); + }, + onStateChange: (state) => { + // This callback will be called whenever the state changes + // The state contains metadata about the query, such as isFetching, isLoading, etc. + console.log('State changed:', state.error, state.isFetching, state.isLoading, state.data); + }, + onError: (error) => { + // This callback will be called if the query fails + console.error('Query error:', error); + }, + onDiff: (diff) => { + // This callback will be called whenever the data changes. + console.log('Data updated:', diff.added, diff.updated); + } +}); +``` + +By default, the `differentialWatch()` method uses a `DEFAULT_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 + 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. 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 + +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 + +`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 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: + +```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(); + +// 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}
+ ))} +
+ ); +}; +```