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 33453fe5..37b94a92 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 955b40a0..8f276d17 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 f34445d8..bd257048 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/docs.json b/docs.json index 9f110723..22badffa 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 db55b725..1c54bd3f 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 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..112fa5ea --- /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 dca3adbc..ffa010f4 100644 --- a/usage/use-case-examples.mdx +++ b/usage/use-case-examples.mdx @@ -14,6 +14,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..2eeef454 --- /dev/null +++ b/usage/use-case-examples/watch-queries.mdx @@ -0,0 +1,455 @@ +--- +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'; + +Watch queries, also known as live queries, are essential for building reactive apps where the UI automatically updates when the underlying data changes. PowerSync's watch functionality allows you to 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. + +Scroll horizontally to find your preferred platform/framework for an example: + + + + + + This method is only being maintained for backwards compatibility purposes. Use the improved `db.query.watch()` API instead (see [Incremental Watch Queries](#incremental-watch-queries) below). + + +The original watch method using the AsyncIterator pattern. This is the foundational watch API that works across all JavaScript environments and is being maintained for backwards compatibility. + + + + + + + + This method is only being maintained for backwards compatibility purposes. Use the improved `db.query.watch()` API instead (see [Incremental Watch Queries](#incremental-watch-queries) below). + + +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 in the query hasn't changed. This can lead to excessive re-renders as components receive updates unnecessarily. + +Incremental watch queries solve this by comparing result sets using configurable comparators and only emitting updates when the comparison detects actual data changes. These queries still query the SQLite DB under the hood on each dependent table change, but compare the result sets and only yield results if a change has been made. + + + **JavaScript Only**: Incremental and differential watch queries are currently only available in the JavaScript SDKs starting from: + * Web v1.25.0 + * React Native v1.23.1 + * Node.js v0.8.1 + + +Basic Syntax: + +```javascript +db.query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }).watch(); +``` + +Scroll horizontally to find your preferred approach for an example: + + + + +`WatchedQuery` class that comes with a better API in that it includes loading, fetching and error states, supports multiple listeners, automatic cleanup on PowerSync close, and the new `updateSettings()` API for dynamic parameter changes. This is the preferred approach for JavaScript SDKs: + +```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 for incremental updates: + +```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 that preserves object references for unchanged items and uses row-level comparators to minimize re-renders. 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'], { + rowComparator: { + keyBy: (item) => item.id, + compareBy: (item) => JSON.stringify(item) + } +}); +``` + + + + +React Suspense hook that preserves object references for unchanged items and uses row-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'], { + rowComparator: { + keyBy: (item) => item.id, + compareBy: (item) => JSON.stringify(item) + } +}); +``` + + + + +Providing a `rowComparator` to the React hooks ensures that components only re-render when the query result actually changes. When combined with React memoization (e.g., `React.memo`) on row components that receive query row objects as props, this approach prevents unnecessary updates at the individual row component level, resulting in more efficient UI rendering. + +```jsx +const TodoListsWidget = () => { + const { data: lists } = useQuery('[SQL]', [...parameters], { rowComparator: DEFAULT_ROW_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}; +}); +``` + + + + + +Existing AsyncIterator API with configurable comparator that compares current and previous result sets, only yielding when the comparator detects changes. Use this if you want to maintain the familiar AsyncIterator pattern from the basic watch query API: + +```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 if you want to maintain the familiar callback pattern from the basic watch query API: + +```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); + } + } + } + ); +}; +``` + + + + + + +# 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. + + + **JavaScript Only**: Incremental and differential watch queries are currently only available in the JavaScript SDKs starting from: + * Web v1.25.0 + * React Native v1.23.1 + * Node.js v0.8.1 + + +Basic syntax: + +```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_ROW_COMPARATOR`. This comparator identifies (keys) each row by its `id` column if present, or otherwise by the JSON string of the entire row. For row comparison, it uses the JSON string representation of the full row. This approach is generally safe and effective for most queries. + +For some queries, performance could be improved by supplying a custom `rowComparator`. Such as comparing by a `hash` column generated or stored in SQLite. These hashes currently require manual implementation. + +```javascript +const pendingLists = db + .query({ + sql: 'SELECT * FROM lists WHERE state = ?', + parameters: ['pending'] + }) + .differentialWatch({ + rowComparator: { + keyBy: (item) => item.id, + compareBy: (item) => item._hash + } + }); +``` + + + The [Yjs Document Collaboration Demo + app](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}
+ ))} +
+ ); +}; +```