From 65fd959b73c4c1c7f6bfcd4546a502a4cbee071e Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Wed, 24 Apr 2024 17:54:12 +0200 Subject: [PATCH 01/18] feat(react): add useQuery and useStatus hooks --- .changeset/strange-spiders-study.md | 11 ++ .../library/widgets/HeaderWidget.tsx | 7 +- demos/example-nextjs/src/app/page.tsx | 6 +- .../app/(app)/(chats)/c/[profile]/index.tsx | 6 +- .../src/app/(app)/(chats)/g/[group]/index.tsx | 6 +- .../app/(app)/(chats)/g/[group]/settings.tsx | 24 ++-- .../src/app/(app)/(chats)/index.tsx | 4 +- .../src/app/(app)/contacts/index.tsx | 6 +- .../src/app/(app)/settings/index.tsx | 4 +- .../src/components/groups/MemberSelector.tsx | 4 +- .../app/views/todos/edit/[id].tsx | 10 +- .../app/views/todos/lists.tsx | 4 +- .../library/widgets/HeaderWidget.tsx | 4 +- .../src/app/views/layout.tsx | 4 +- .../src/app/views/sql-console/page.tsx | 4 +- .../src/app/views/todo-lists/edit/page.tsx | 10 +- .../components/widgets/TodoListsWidget.tsx | 4 +- .../src/app/editor/page.tsx | 4 +- .../src/app/sql-console/page.tsx | 4 +- packages/react/README.md | 4 +- packages/react/package.json | 1 - packages/react/src/hooks/usePowerSyncQuery.ts | 1 + .../react/src/hooks/usePowerSyncStatus.ts | 1 + .../src/hooks/usePowerSyncWatchedQuery.ts | 1 + packages/react/src/hooks/useQuery.ts | 111 ++++++++++++++++++ packages/react/src/hooks/useStatus.ts | 19 +++ packages/react/src/index.ts | 2 + packages/react/tests/useQuery.test.tsx | 105 +++++++++++++++++ ...SyncStatus.test.tsx => useStatus.test.tsx} | 12 +- pnpm-lock.yaml | 27 +---- 30 files changed, 314 insertions(+), 96 deletions(-) create mode 100644 .changeset/strange-spiders-study.md create mode 100644 packages/react/src/hooks/useQuery.ts create mode 100644 packages/react/src/hooks/useStatus.ts create mode 100644 packages/react/tests/useQuery.test.tsx rename packages/react/tests/{usePowerSyncStatus.test.tsx => useStatus.test.tsx} (79%) diff --git a/.changeset/strange-spiders-study.md b/.changeset/strange-spiders-study.md new file mode 100644 index 000000000..5cbe2e312 --- /dev/null +++ b/.changeset/strange-spiders-study.md @@ -0,0 +1,11 @@ +--- +"react-native-supabase-group-chat": minor +"react-native-supabase-todolist": minor +"yjs-react-supabase-text-collab": minor +"django-react-native-todolist": minor +"react-supabase-todolist": minor +"example-nextjs": minor +"@powersync/react": minor +--- + +Deprecate usePowerSyncStatus, usePowerSyncQuery and usePowerSyncWatchedQuery in favor of useQuery and useStatus diff --git a/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx b/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx index 6dfcc98b3..f139be2a5 100644 --- a/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx +++ b/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx @@ -3,7 +3,7 @@ import { Alert, Text } from 'react-native'; import { Icon } from 'react-native-elements'; import { useNavigation } from 'expo-router'; import { useSystem } from '../stores/system'; -import { usePowerSyncStatus } from '@powersync/react'; +import { useStatus } from '@powersync/react'; import { Header } from 'react-native-elements'; import { observer } from 'mobx-react-lite'; import { DrawerActions } from '@react-navigation/native'; @@ -13,7 +13,7 @@ export const HeaderWidget: React.FC<{ }> = observer((props) => { const { title } = props; const { powersync } = useSystem(); - const status = usePowerSyncStatus(); + const status = useStatus(); const navigation = useNavigation(); return ( @@ -39,8 +39,7 @@ export const HeaderWidget: React.FC<{ onPress={() => { Alert.alert( 'Status', - `${status.connected ? 'Connected' : 'Disconnected'}. \nLast Synced at ${ - status.lastSyncedAt?.toISOString() ?? '-' + `${status.connected ? 'Connected' : 'Disconnected'}. \nLast Synced at ${status.lastSyncedAt?.toISOString() ?? '-' }\nVersion: ${powersync.sdkVersion}` ); }} diff --git a/demos/example-nextjs/src/app/page.tsx b/demos/example-nextjs/src/app/page.tsx index cf4e6b92d..d61e47881 100644 --- a/demos/example-nextjs/src/app/page.tsx +++ b/demos/example-nextjs/src/app/page.tsx @@ -2,13 +2,11 @@ import React, { useEffect } from 'react'; import { CircularProgress, Grid, ListItem, styled } from '@mui/material'; -import { useRouter } from 'next/navigation'; -import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/react'; +import { usePowerSync, useQuery } from '@powersync/react'; export default function EntryPage() { - const router = useRouter(); const db = usePowerSync(); - const customers = usePowerSyncWatchedQuery('SELECT id, name FROM customers'); + const { data: customers } = useQuery('SELECT id, name FROM customers'); useEffect(() => { // Insert some test data diff --git a/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/c/[profile]/index.tsx b/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/c/[profile]/index.tsx index 06ca93313..e2a1a52b2 100644 --- a/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/c/[profile]/index.tsx +++ b/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/c/[profile]/index.tsx @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/react-native'; +import { usePowerSync, useQuery } from '@powersync/react-native'; import { FlashList } from '@shopify/flash-list'; import { Stack, useLocalSearchParams } from 'expo-router'; import { useEffect, useState } from 'react'; @@ -13,12 +13,12 @@ export default function ChatsChatIndex() { const { profile: profileId } = useLocalSearchParams<{ profile: string }>(); const { user } = useAuth(); const powerSync = usePowerSync(); - const profiles = usePowerSyncWatchedQuery('SELECT id, name, handle, demo FROM profiles WHERE id = ?', [profileId]); + const { data: profiles } = useQuery('SELECT id, name, handle, demo FROM profiles WHERE id = ?', [profileId]); const profile = profiles.length ? profiles[0] : undefined; const [draftId, setDraftId] = useState(); const [listMessages, setListMessages] = useState([]); - const messages = usePowerSyncWatchedQuery( + const { data: messages } = useQuery( 'SELECT sender_id, content, created_at FROM messages WHERE (((sender_id = ?1 AND recipient_id = ?2) OR (sender_id = ?2 AND recipient_id = ?1)) AND NOT (sender_id = ?1 AND sent_at IS NULL)) ORDER BY created_at ASC', [user?.id, profile?.id] ); diff --git a/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/g/[group]/index.tsx b/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/g/[group]/index.tsx index 3b9ab323f..d838a7bfb 100644 --- a/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/g/[group]/index.tsx +++ b/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/g/[group]/index.tsx @@ -1,4 +1,4 @@ -import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/react-native'; +import { usePowerSync, useQuery } from '@powersync/react-native'; import { FlashList } from '@shopify/flash-list'; import { Stack, useLocalSearchParams } from 'expo-router'; import { useState } from 'react'; @@ -12,10 +12,10 @@ export default function ChatsChatIndex() { const { group: groupId } = useLocalSearchParams<{ group: string }>(); const { user } = useAuth(); const powerSync = usePowerSync(); - const groups = usePowerSyncWatchedQuery('SELECT id, name FROM groups WHERE id = ?', [groupId]); + const { data: groups } = useQuery('SELECT id, name FROM groups WHERE id = ?', [groupId]); const group = groups.length ? groups[0] : undefined; - const messages = usePowerSyncWatchedQuery( + const { data: messages } = useQuery( 'SELECT sender_id, content, created_at FROM messages WHERE group_id = ? ORDER BY created_at ASC', [group?.id] ); diff --git a/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/g/[group]/settings.tsx b/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/g/[group]/settings.tsx index 7a042a80b..6ab1db290 100644 --- a/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/g/[group]/settings.tsx +++ b/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/g/[group]/settings.tsx @@ -1,4 +1,4 @@ -import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/react-native'; +import { usePowerSync, useQuery } from '@powersync/react-native'; import { Save, Trash, XCircle } from '@tamagui/lucide-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useEffect, useState } from 'react'; @@ -16,8 +16,8 @@ export default function GroupSettings() { const [selectedContacts, setSelectedContacts] = useState>(new Set()); const powerSync = usePowerSync(); - const groups = usePowerSyncWatchedQuery('SELECT name FROM groups WHERE id = ?', [groupId]); - const groupMembers = usePowerSyncWatchedQuery('SELECT profile_id FROM memberships WHERE group_id = ?', [groupId]); + const { data: groups } = useQuery('SELECT name FROM groups WHERE id = ?', [groupId]); + const { data: groupMembers } = useQuery('SELECT profile_id FROM memberships WHERE group_id = ?', [groupId]); useEffect(() => { if (groups.length > 0) { @@ -74,16 +74,13 @@ export default function GroupSettings() { await powerSync.writeTransaction(async (tx) => { try { - await tx.executeAsync('UPDATE groups SET name= ? WHERE id = ?', [name, groupId]); + await tx.execute('UPDATE groups SET name= ? WHERE id = ?', [name, groupId]); for (const profileId of removedContacts) { - const result = await tx.executeAsync('DELETE FROM memberships WHERE group_id = ? AND profile_id = ?', [ - groupId, - profileId - ]); + await tx.execute('DELETE FROM memberships WHERE group_id = ? AND profile_id = ?', [groupId, profileId]); } for (const profileId of addedContacts) { const membershipId = uuid(); - const result = await tx.executeAsync( + await tx.execute( 'INSERT INTO memberships (id, group_id, profile_id, created_at) VALUES (?, ?, ?, datetime())', [membershipId, groupId, profileId] ); @@ -99,9 +96,9 @@ export default function GroupSettings() { async function deleteTransaction() { await powerSync.writeTransaction(async (tx) => { try { - await tx.executeAsync('DELETE FROM memberships WHERE group_id = ?', [groupId]); - await tx.executeAsync('DELETE FROM messages WHERE group_id = ?', [groupId]); - await tx.executeAsync('DELETE FROM groups WHERE id = ?', [groupId]); + await tx.execute('DELETE FROM memberships WHERE group_id = ?', [groupId]); + await tx.execute('DELETE FROM messages WHERE group_id = ?', [groupId]); + await tx.execute('DELETE FROM groups WHERE id = ?', [groupId]); router.back(); } catch (error) { @@ -148,8 +145,7 @@ export default function GroupSettings() { backgroundColor="$red10" color="white" onPress={handleDelete} - margin="$3" - > + margin="$3"> Delete group diff --git a/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/index.tsx b/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/index.tsx index 9bd475969..7200f63d7 100644 --- a/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/index.tsx +++ b/demos/react-native-supabase-group-chat/src/app/(app)/(chats)/index.tsx @@ -1,4 +1,4 @@ -import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/react-native'; +import { usePowerSync, useQuery } from '@powersync/react-native'; import { MessageSquare, Plus } from '@tamagui/lucide-icons'; import { Link, useNavigation } from 'expo-router'; import { useEffect, useState } from 'react'; @@ -24,7 +24,7 @@ export default function ChatsIndex() { return unsubscribe; }, [navigation]); - const chats = usePowerSyncWatchedQuery( + const { data: chats } = useQuery( `SELECT profiles.id as partner_id, profiles.name as name, profiles.handle as handle, 'contact' as type, m.created_at as last_message_at FROM chats LEFT JOIN profiles on chats.id = profiles.id LEFT JOIN (SELECT * FROM messages WHERE (sender_id, recipient_id, created_at) IN (SELECT sender_id, recipient_id, MAX(created_at) FROM messages GROUP BY group_id)) as m ON m.recipient_id = chats.id OR m.sender_id = chats.id WHERE (name LIKE '%' || ?1 || '%' OR handle LIKE '%' || ?1 || '%') GROUP BY profiles.id UNION SELECT groups.id as partner_id, groups.name as name, '' as handle, 'group' as type, m.created_at as last_message_at FROM groups LEFT JOIN (SELECT * FROM messages WHERE (group_id, created_at) IN (SELECT group_id, MAX(created_at) FROM messages GROUP BY group_id)) as m ON m.group_id = groups.id WHERE (name LIKE '%' || ?1 || '%') GROUP BY groups.id ORDER BY last_message_at DESC`, [search] diff --git a/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx b/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx index e91b1b68f..6d476a3ca 100644 --- a/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx +++ b/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/react-native'; +import { usePowerSync, useQuery } from '@powersync/react-native'; import { Search, Shuffle } from '@tamagui/lucide-icons'; import { useState } from 'react'; import { Button, Input, XStack, YStack } from 'tamagui'; @@ -17,7 +17,7 @@ export default function ContactsIndex() { const [search, setSearch] = useState(''); const [profiles, setProfiles] = useState([]); - const contacts = usePowerSyncWatchedQuery( + const { data: contacts } = useQuery( "SELECT contacts.id, profiles.id as profile_id, profiles.name, profiles.handle, 'contact' as type FROM contacts LEFT JOIN profiles ON contacts.profile_id = profiles.id WHERE (profiles.name LIKE '%' || ?1 || '%' OR profiles.handle LIKE '%' || ?1 || '%') ORDER BY name ASC", [search] ); @@ -112,7 +112,7 @@ export default function ContactsIndex() { icon={} backgroundColor="$brand1" borderRadius="$3" - // circular + // circular /> diff --git a/demos/react-native-supabase-group-chat/src/app/(app)/settings/index.tsx b/demos/react-native-supabase-group-chat/src/app/(app)/settings/index.tsx index 842cc0579..f53a1d1ea 100644 --- a/demos/react-native-supabase-group-chat/src/app/(app)/settings/index.tsx +++ b/demos/react-native-supabase-group-chat/src/app/(app)/settings/index.tsx @@ -1,4 +1,4 @@ -import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/react-native'; +import { usePowerSync, useQuery } from '@powersync/react-native'; import { useEffect, useState } from 'react'; import { Button, Input, Label, Switch, Text, XStack, YStack } from 'tamagui'; @@ -10,7 +10,7 @@ export default function SettingsIndex() { const [name, setName] = useState(''); const [handle, setHandle] = useState(''); - const profiles = usePowerSyncWatchedQuery('SELECT * FROM profiles WHERE id = ?', [user?.id]); + const { data: profiles } = useQuery('SELECT * FROM profiles WHERE id = ?', [user?.id]); useEffect(() => { if (profiles.length > 0) { diff --git a/demos/react-native-supabase-group-chat/src/components/groups/MemberSelector.tsx b/demos/react-native-supabase-group-chat/src/components/groups/MemberSelector.tsx index 24af0988b..42303e914 100644 --- a/demos/react-native-supabase-group-chat/src/components/groups/MemberSelector.tsx +++ b/demos/react-native-supabase-group-chat/src/components/groups/MemberSelector.tsx @@ -1,4 +1,4 @@ -import { usePowerSyncWatchedQuery } from '@powersync/react-native'; +import { useQuery } from '@powersync/react-native'; import { CheckCircle2, Circle } from '@tamagui/lucide-icons'; import { useState } from 'react'; import { Input, ListItem, XStack, YStack } from 'tamagui'; @@ -15,7 +15,7 @@ export function MemberSelector({ }) { const [search, setSearch] = useState(''); - const contacts = usePowerSyncWatchedQuery( + const { data: contacts } = useQuery( "SELECT contacts.id, profiles.id as profile_id, profiles.name, profiles.handle, 'contact' as type FROM contacts LEFT JOIN profiles ON contacts.profile_id = profiles.id WHERE (profiles.name LIKE '%' || ?1 || '%' OR profiles.handle LIKE '%' || ?1 || '%') ORDER BY name ASC", [search] ); diff --git a/demos/react-native-supabase-todolist/app/views/todos/edit/[id].tsx b/demos/react-native-supabase-todolist/app/views/todos/edit/[id].tsx index 5dc13f273..93b70ce43 100644 --- a/demos/react-native-supabase-todolist/app/views/todos/edit/[id].tsx +++ b/demos/react-native-supabase-todolist/app/views/todos/edit/[id].tsx @@ -1,5 +1,5 @@ import { ATTACHMENT_TABLE, AttachmentRecord } from '@powersync/attachments'; -import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/react-native'; +import { usePowerSync, useQuery } from '@powersync/react-native'; import { CameraCapturedPicture } from 'expo-camera'; import _ from 'lodash'; import * as React from 'react'; @@ -34,11 +34,11 @@ const TodoView: React.FC = () => { const params = useLocalSearchParams<{ id: string }>(); const listID = params.id; - const [listRecord] = usePowerSyncWatchedQuery<{ name: string }>(`SELECT name FROM ${LIST_TABLE} WHERE id = ?`, [ - listID - ]); + const { + data: [listRecord] + } = useQuery<{ name: string }>(`SELECT name FROM ${LIST_TABLE} WHERE id = ?`, [listID]); - const todos = usePowerSyncWatchedQuery( + const { data: todos } = useQuery( ` SELECT ${TODO_TABLE}.id AS todo_id, diff --git a/demos/react-native-supabase-todolist/app/views/todos/lists.tsx b/demos/react-native-supabase-todolist/app/views/todos/lists.tsx index 6f9171a42..19bc3d53c 100644 --- a/demos/react-native-supabase-todolist/app/views/todos/lists.tsx +++ b/demos/react-native-supabase-todolist/app/views/todos/lists.tsx @@ -7,7 +7,7 @@ import prompt from 'react-native-prompt-android'; import { router, Stack } from 'expo-router'; import { LIST_TABLE, TODO_TABLE, ListRecord } from '../../../library/powersync/AppSchema'; import { useSystem } from '../../../library/powersync/system'; -import { usePowerSyncWatchedQuery } from '@powersync/react-native'; +import { useQuery } from '@powersync/react-native'; import { ListItemWidget } from '../../../library/widgets/ListItemWidget'; const description = (total: number, completed: number = 0) => { @@ -16,7 +16,7 @@ const description = (total: number, completed: number = 0) => { const ListsViewWidget: React.FC = () => { const system = useSystem(); - const listRecords = usePowerSyncWatchedQuery(` + const { data: listRecords } = useQuery(` SELECT ${LIST_TABLE}.*, COUNT(${TODO_TABLE}.id) AS total_tasks, SUM(CASE WHEN ${TODO_TABLE}.completed = true THEN 1 ELSE 0 END) as completed_tasks FROM diff --git a/demos/react-native-supabase-todolist/library/widgets/HeaderWidget.tsx b/demos/react-native-supabase-todolist/library/widgets/HeaderWidget.tsx index 101441c59..3ed50f12a 100644 --- a/demos/react-native-supabase-todolist/library/widgets/HeaderWidget.tsx +++ b/demos/react-native-supabase-todolist/library/widgets/HeaderWidget.tsx @@ -3,7 +3,7 @@ import { Alert, Text } from 'react-native'; import { Icon } from 'react-native-elements'; import { useNavigation } from 'expo-router'; import { Header } from 'react-native-elements'; -import { usePowerSyncStatus } from '@powersync/react'; +import { useStatus } from '@powersync/react'; import { DrawerActions } from '@react-navigation/native'; import { useSystem } from '../powersync/system'; @@ -13,7 +13,7 @@ export const HeaderWidget: React.FC<{ const system = useSystem(); const { powersync } = system; const navigation = useNavigation(); - const status = usePowerSyncStatus(); + const status = useStatus(); const { title } = props; return ( diff --git a/demos/react-supabase-todolist/src/app/views/layout.tsx b/demos/react-supabase-todolist/src/app/views/layout.tsx index d78f487c4..fbf6db2dd 100644 --- a/demos/react-supabase-todolist/src/app/views/layout.tsx +++ b/demos/react-supabase-todolist/src/app/views/layout.tsx @@ -25,13 +25,13 @@ import React from 'react'; import { useNavigationPanel } from '@/components/navigation/NavigationPanelContext'; import { useSupabase } from '@/components/providers/SystemProvider'; -import { usePowerSync, usePowerSyncStatus } from '@powersync/react'; +import { usePowerSync, useStatus } from '@powersync/react'; import { useNavigate } from 'react-router-dom'; import { LOGIN_ROUTE, SQL_CONSOLE_ROUTE, TODO_LISTS_ROUTE } from '@/app/router'; export default function ViewsLayout({ children }: { children: React.ReactNode }) { const powerSync = usePowerSync(); - const status = usePowerSyncStatus(); + const status = useStatus(); const supabase = useSupabase(); const navigate = useNavigate(); diff --git a/demos/react-supabase-todolist/src/app/views/sql-console/page.tsx b/demos/react-supabase-todolist/src/app/views/sql-console/page.tsx index 3bd67307a..6b0a13e1d 100644 --- a/demos/react-supabase-todolist/src/app/views/sql-console/page.tsx +++ b/demos/react-supabase-todolist/src/app/views/sql-console/page.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { usePowerSyncWatchedQuery } from '@powersync/react'; +import { useQuery } from '@powersync/react'; import { Box, Button, Grid, TextField, styled } from '@mui/material'; import { DataGrid } from '@mui/x-data-grid'; import { NavigationPage } from '@/components/navigation/NavigationPage'; @@ -14,7 +14,7 @@ const DEFAULT_QUERY = 'SELECT * FROM lists'; export default function SQLConsolePage() { const inputRef = React.useRef(); const [query, setQuery] = React.useState(DEFAULT_QUERY); - const querySQLResult = usePowerSyncWatchedQuery(query); + const { data: querySQLResult } = useQuery(query); const queryDataGridResult = React.useMemo(() => { const firstItem = querySQLResult?.[0]; diff --git a/demos/react-supabase-todolist/src/app/views/todo-lists/edit/page.tsx b/demos/react-supabase-todolist/src/app/views/todo-lists/edit/page.tsx index 5287b1dec..7c3e88fe7 100644 --- a/demos/react-supabase-todolist/src/app/views/todo-lists/edit/page.tsx +++ b/demos/react-supabase-todolist/src/app/views/todo-lists/edit/page.tsx @@ -1,7 +1,7 @@ import { useSupabase } from '@/components/providers/SystemProvider'; import { TodoItemWidget } from '@/components/widgets/TodoItemWidget'; import { LISTS_TABLE, TODOS_TABLE, TodoRecord } from '@/library/powersync/AppSchema'; -import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/react'; +import { usePowerSync, useQuery } from '@powersync/react'; import AddIcon from '@mui/icons-material/Add'; import { Box, @@ -32,11 +32,11 @@ const TodoEditSection = () => { const supabase = useSupabase(); const { id: listID } = useParams(); - const [listRecord] = usePowerSyncWatchedQuery<{ name: string }>(`SELECT name FROM ${LISTS_TABLE} WHERE id = ?`, [ - listID - ]); + const { + data: [listRecord] + } = useQuery<{ name: string }>(`SELECT name FROM ${LISTS_TABLE} WHERE id = ?`, [listID]); - const todos = usePowerSyncWatchedQuery( + const { data: todos } = useQuery( `SELECT * FROM ${TODOS_TABLE} WHERE list_id=? ORDER BY created_at DESC, id`, [listID] ); diff --git a/demos/react-supabase-todolist/src/components/widgets/TodoListsWidget.tsx b/demos/react-supabase-todolist/src/components/widgets/TodoListsWidget.tsx index 1168a76e8..044284fc2 100644 --- a/demos/react-supabase-todolist/src/components/widgets/TodoListsWidget.tsx +++ b/demos/react-supabase-todolist/src/components/widgets/TodoListsWidget.tsx @@ -1,6 +1,6 @@ import { TODO_LISTS_ROUTE } from '@/app/router'; import { LISTS_TABLE, ListRecord, TODOS_TABLE } from '@/library/powersync/AppSchema'; -import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/react'; +import { usePowerSync, useQuery } from '@powersync/react'; import { List } from '@mui/material'; import { useNavigate } from 'react-router-dom'; import { ListItemWidget } from './ListItemWidget'; @@ -17,7 +17,7 @@ export function TodoListsWidget(props: TodoListsWidgetProps) { const powerSync = usePowerSync(); const navigate = useNavigate(); - const listRecords = usePowerSyncWatchedQuery(` + const { data: listRecords } = useQuery(` SELECT ${LISTS_TABLE}.*, COUNT(${TODOS_TABLE}.id) AS total_tasks, SUM(CASE WHEN ${TODOS_TABLE}.completed = true THEN 1 ELSE 0 END) as completed_tasks FROM diff --git a/demos/yjs-react-supabase-text-collab/src/app/editor/page.tsx b/demos/yjs-react-supabase-text-collab/src/app/editor/page.tsx index d94b848e2..1709b5e06 100644 --- a/demos/yjs-react-supabase-text-collab/src/app/editor/page.tsx +++ b/demos/yjs-react-supabase-text-collab/src/app/editor/page.tsx @@ -1,4 +1,4 @@ -import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/react'; +import { usePowerSync, useQuery } from '@powersync/react'; import { Box, Container, Typography } from '@mui/material'; import { useEffect, useMemo, useState } from 'react'; @@ -37,7 +37,7 @@ export default function EditorPage() { }, [ydoc, powerSync]); // watch for total number of document updates changing to update the counter - const docUpdatesCount = usePowerSyncWatchedQuery( + const { data: docUpdatesCount } = useQuery( 'SELECT COUNT(*) as total_updates FROM document_updates WHERE document_id=?', [documentId] ); diff --git a/demos/yjs-react-supabase-text-collab/src/app/sql-console/page.tsx b/demos/yjs-react-supabase-text-collab/src/app/sql-console/page.tsx index fa020a63e..f15b56a20 100644 --- a/demos/yjs-react-supabase-text-collab/src/app/sql-console/page.tsx +++ b/demos/yjs-react-supabase-text-collab/src/app/sql-console/page.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { usePowerSyncWatchedQuery } from '@powersync/react'; +import { useQuery } from '@powersync/react'; import { Box, Button, Grid, TextField, styled } from '@mui/material'; import { DataGrid } from '@mui/x-data-grid'; @@ -8,7 +8,7 @@ const DEFAULT_QUERY = 'SELECT * FROM documents'; export default function SQLConsolePage() { const inputRef = React.useRef(); const [query, setQuery] = React.useState(DEFAULT_QUERY); - const querySQLResult = usePowerSyncWatchedQuery(query); + const { data: querySQLResult } = useQuery(query); const queryDataGridResult = React.useMemo(() => { const firstItem = querySQLResult?.[0]; diff --git a/packages/react/README.md b/packages/react/README.md index e3f44d289..f9d354d7b 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -47,10 +47,10 @@ export const TodoListDisplay = () => { The provided PowerSync client status is available with the `usePowerSyncStatus` hook. ```JSX -import { usePowerSyncStatus } from "@powersync/react"; +import { useStatus } from "@powersync/react"; const Component = () => { - const status = usePowerSyncStatus(); + const status = useStatus(); return
status.connected ? 'wifi' : 'wifi-off' diff --git a/packages/react/package.json b/packages/react/package.json index c3dc52d0e..6c4102c09 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -35,7 +35,6 @@ }, "devDependencies": { "@testing-library/react": "^15.0.2", - "@testing-library/react-hooks": "^8.0.1", "@types/react": "^18.2.34", "jsdom": "^24.0.0", "react": "18.2.0", diff --git a/packages/react/src/hooks/usePowerSyncQuery.ts b/packages/react/src/hooks/usePowerSyncQuery.ts index 955736ddd..86db3dc9b 100644 --- a/packages/react/src/hooks/usePowerSyncQuery.ts +++ b/packages/react/src/hooks/usePowerSyncQuery.ts @@ -2,6 +2,7 @@ import React from 'react'; import { usePowerSync } from './PowerSyncContext'; /** + * @deprecated use {@link useQuery} instead * A hook to access a single static query. * For an updated result, use {@link usePowerSyncWatchedQuery} instead */ diff --git a/packages/react/src/hooks/usePowerSyncStatus.ts b/packages/react/src/hooks/usePowerSyncStatus.ts index 13bab3e17..b565f351a 100644 --- a/packages/react/src/hooks/usePowerSyncStatus.ts +++ b/packages/react/src/hooks/usePowerSyncStatus.ts @@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from 'react'; import { PowerSyncContext } from './PowerSyncContext'; /** + * @deprecated Use {@link useStatus} instead. * Custom hook that provides access to the current status of PowerSync. * @returns The PowerSync Database status. * @example diff --git a/packages/react/src/hooks/usePowerSyncWatchedQuery.ts b/packages/react/src/hooks/usePowerSyncWatchedQuery.ts index c7bb3daaf..31c008f8a 100644 --- a/packages/react/src/hooks/usePowerSyncWatchedQuery.ts +++ b/packages/react/src/hooks/usePowerSyncWatchedQuery.ts @@ -3,6 +3,7 @@ import React from 'react'; import { usePowerSync } from './PowerSyncContext'; /** + * @deprecated use {@link useQuery} instead * A hook to access the results of a watched query. * @example * export const Component = () => { diff --git a/packages/react/src/hooks/useQuery.ts b/packages/react/src/hooks/useQuery.ts new file mode 100644 index 000000000..70415ee58 --- /dev/null +++ b/packages/react/src/hooks/useQuery.ts @@ -0,0 +1,111 @@ +import { SQLWatchOptions } from '@powersync/common'; +import React from 'react'; +import { usePowerSync } from './PowerSyncContext'; + +interface AdditionalOptions extends Omit { + runQueryOnce?: boolean; +} + +export type WatchedQueryResult = { + data: T[]; + /** + * Indicates the initial loading state (hard loading). Loading becomes false once the first set of results from the watched query is available or an error occurs. + */ + isLoading: boolean; + error: Error; + /** + * Function used to run the query again. + */ + refresh?: () => Promise; +}; + +/** + * A hook to access the results of a watched query. + * @example + * ```tsx + * export const Component = () => { + * const lists = useQuery('SELECT * from lists'); + * + * return + * {lists.map((l) => ( + * {JSON.stringify(l)} + * ))} + * + * } + * ``` + */ +export const useQuery = ( + sqlStatement: string, + parameters: any[] = [], + options: AdditionalOptions = {} +): WatchedQueryResult => { + const powerSync = usePowerSync(); + if (!powerSync) { + return { isLoading: false, data: [], error: new Error('PowerSync not configured.') }; + } + + const [data, setData] = React.useState([]); + const [error, setError] = React.useState(undefined); + const [isLoading, setIsLoading] = React.useState(true); + + const memoizedParams = React.useMemo(() => parameters, [...parameters]); + const memoizedOptions = React.useMemo(() => options, [JSON.stringify(options)]); + const abortController = React.useRef(new AbortController()); + + const handleResult = (result: T[]) => { + setIsLoading(false); + setData(result); + setError(undefined); + }; + + const handleError = (e: Error) => { + setIsLoading(false); + setData([]); + const wrappedError = new Error('PowerSync failed to fetch data: ' + e.message); + wrappedError.cause = e; + setError(wrappedError); + }; + + const fetchData = async () => { + setIsLoading(true); + try { + const result = await powerSync.getAll(sqlStatement, parameters); + handleResult(result); + } catch (e) { + handleError(e); + } + }; + + React.useEffect(() => { + // Abort any previous watches + abortController.current?.abort(); + abortController.current = new AbortController(); + + if (options.runQueryOnce) { + fetchData(); + } else { + powerSync.watch( + sqlStatement, + parameters, + { + onResult(results) { + handleResult(results.rows?._array ?? []); + }, + onError(e) { + handleError(e); + } + }, + { + ...options, + signal: abortController.current.signal + } + ); + } + + return () => { + abortController.current?.abort(); + }; + }, [powerSync, sqlStatement, memoizedParams, memoizedOptions]); + + return { isLoading, data, error, refresh: fetchData }; +}; diff --git a/packages/react/src/hooks/useStatus.ts b/packages/react/src/hooks/useStatus.ts new file mode 100644 index 000000000..20ffa6dc0 --- /dev/null +++ b/packages/react/src/hooks/useStatus.ts @@ -0,0 +1,19 @@ +import { usePowerSyncStatus } from './usePowerSyncStatus'; + +/** + * Custom hook that provides access to the current status of PowerSync. + * @returns The PowerSync Database status. + * @example + * ```JSX + * import { useStatus } from "@powersync/react"; + * + * const Component = () => { + * const status = useStatus(); + * + * return
+ * status.connected ? 'wifi' : 'wifi-off' + *
+ * }; + * ``` + */ +export const useStatus = usePowerSyncStatus; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 9f35b3e8e..fff9be305 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,4 +1,6 @@ export * from './hooks/PowerSyncContext'; export { usePowerSyncQuery } from './hooks/usePowerSyncQuery'; +export { useStatus } from './hooks/useStatus'; +export { useQuery } from './hooks/useQuery'; export { usePowerSyncWatchedQuery } from './hooks/usePowerSyncWatchedQuery'; export { usePowerSyncStatus } from './hooks/usePowerSyncStatus'; diff --git a/packages/react/tests/useQuery.test.tsx b/packages/react/tests/useQuery.test.tsx new file mode 100644 index 000000000..43584e270 --- /dev/null +++ b/packages/react/tests/useQuery.test.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { vi, describe, expect, it, afterEach } from 'vitest'; +import { useQuery } from '../src/hooks/useQuery'; +import { PowerSyncContext } from '../src/hooks/PowerSyncContext'; + +const mockPowerSync = { + currentStatus: { status: 'initial' }, + registerListener: vi.fn(() => ({ + statusChanged: vi.fn(() => 'updated') + })), + watch: vi.fn(), + getAll: vi.fn(() => ['list1', 'list2']) +}; + +vi.mock('./PowerSyncContext', () => ({ + useContext: vi.fn(() => mockPowerSync) +})); + +describe('useQuery', () => { + afterEach(() => { + vi.resetAllMocks(); + }); + + it('should error when PowerSync is not set', () => { + const { result } = renderHook(() => useQuery('SELECT * from lists')); + expect(result.current.error).toEqual(Error('PowerSync not configured.')); + expect(result.current.isLoading).toEqual(false); + expect(result.current.data).toEqual([]); + }); + + it('should set isLoading to true on initial load', async () => { + const wrapper = ({ children }) => ( + {children} + ); + + const { result } = renderHook(() => useQuery('SELECT * from lists'), { wrapper }); + expect(result.current.isLoading).toEqual(true); + }); + + it('should run the query once if runQueryOnce flag is set', async () => { + const wrapper = ({ children }) => ( + {children} + ); + + const { result } = renderHook(() => useQuery('SELECT * from lists', [], { runQueryOnce: true }), { wrapper }); + expect(result.current.isLoading).toEqual(true); + + waitFor( + () => { + expect(result.current.isLoading).toEqual(false); + expect(result.current.data).toEqual(['list1', 'list2']); + expect(result.current.isLoading).toEqual(false); + expect(mockPowerSync.watch).not.toHaveBeenCalled(); + expect(mockPowerSync.getAll).toHaveBeenCalledTimes(1); + }, + { timeout: 100 } + ); + }); + + it('should rerun the query when refresh is used', async () => { + const wrapper = ({ children }) => ( + {children} + ); + + const { result } = renderHook(() => useQuery('SELECT * from lists', [], { runQueryOnce: true }), { wrapper }); + expect(result.current.isLoading).toEqual(true); + + let refresh; + + waitFor( + () => { + refresh = result.current.refresh; + expect(result.current.isLoading).toEqual(false); + expect(mockPowerSync.getAll).toHaveBeenCalledTimes(1); + }, + { timeout: 100 } + ); + + await refresh(); + expect(mockPowerSync.getAll).toHaveBeenCalledTimes(2); + }); + + it('should set error when error occurs and runQueryOnce flag is set', async () => { + const mockPowerSyncError = { + currentStatus: { status: 'initial' }, + registerListener: vi.fn(() => ({ + statusChanged: vi.fn(() => 'updated') + })), + watch: vi.fn(), + getAll: vi.fn(() => { + throw new Error('some error'); + }) + }; + + const wrapper = ({ children }) => ( + {children} + ); + + const { result } = renderHook(() => useQuery('SELECT * from lists', [], { runQueryOnce: true }), { wrapper }); + expect(result.current.error).toEqual(Error('PowerSync failed to fetch data: some error')); + }); + + // TODO: Add tests for powersync.watch path +}); diff --git a/packages/react/tests/usePowerSyncStatus.test.tsx b/packages/react/tests/useStatus.test.tsx similarity index 79% rename from packages/react/tests/usePowerSyncStatus.test.tsx rename to packages/react/tests/useStatus.test.tsx index d67d1406c..a0727661a 100644 --- a/packages/react/tests/usePowerSyncStatus.test.tsx +++ b/packages/react/tests/useStatus.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, renderHook } from '@testing-library/react'; import { vi, describe, expect, it, afterEach } from 'vitest'; -import { usePowerSyncStatus } from '../src/hooks/usePowerSyncStatus'; +import { useStatus } from '../src/hooks/useStatus'; import { PowerSyncContext } from '../src/hooks/PowerSyncContext'; const mockPowerSync = { @@ -15,7 +15,7 @@ vi.mock('./PowerSyncContext', () => ({ useContext: vi.fn(() => mockPowerSync) })); -describe('usePowerSyncStatus', () => { +describe('useStatus', () => { afterEach(() => { vi.resetAllMocks(); }); @@ -25,7 +25,7 @@ describe('usePowerSyncStatus', () => { {children} ); - const { result } = renderHook(() => usePowerSyncStatus(), { wrapper }); + const { result } = renderHook(() => useStatus(), { wrapper }); expect(result.current).toEqual(mockPowerSync.currentStatus); }); @@ -35,7 +35,7 @@ describe('usePowerSyncStatus', () => { {children} ); - const { result } = renderHook(() => usePowerSyncStatus(), { wrapper }); + const { result } = renderHook(() => useStatus(), { wrapper }); act(() => { mockPowerSync.registerListener.mockResolvedValue({ statusChanged: vi.fn(() => 'updated') }); @@ -49,7 +49,7 @@ describe('usePowerSyncStatus', () => { {children} ); - const { unmount } = renderHook(() => usePowerSyncStatus(), { wrapper }); + const { unmount } = renderHook(() => useStatus(), { wrapper }); const listenerUnsubscribe = mockPowerSync.registerListener; unmount(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6554f6109..6ece12390 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1210,9 +1210,6 @@ importers: '@testing-library/react': specifier: ^15.0.2 version: 15.0.2(react-dom@18.2.0)(react@18.2.0) - '@testing-library/react-hooks': - specifier: ^8.0.1 - version: 8.0.1(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@types/react': specifier: ^18.2.34 version: 18.2.79 @@ -16250,29 +16247,6 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/react-hooks@8.0.1(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==} - engines: {node: '>=12'} - peerDependencies: - '@types/react': ^16.9.0 || ^17.0.0 - react: ^16.9.0 || ^17.0.0 - react-dom: ^16.9.0 || ^17.0.0 - react-test-renderer: ^16.9.0 || ^17.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - react-dom: - optional: true - react-test-renderer: - optional: true - dependencies: - '@babel/runtime': 7.24.0 - '@types/react': 18.2.79 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-error-boundary: 3.1.4(react@18.2.0) - dev: true - /@testing-library/react@15.0.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-5mzIpuytB1ctpyywvyaY2TAAUQVCZIGqwiqFQf6u9lvj/SJQepGUzNV18Xpk+NLCaCE2j7CWrZE0tEf9xLZYiQ==} engines: {node: '>=18'} @@ -30788,6 +30762,7 @@ packages: dependencies: '@babel/runtime': 7.24.0 react: 18.2.0 + dev: false /react-error-overlay@6.0.11: resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} From e3a7ec1d34ce623515dfda83fd8af69e1590e4d6 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Wed, 24 Apr 2024 18:02:42 +0200 Subject: [PATCH 02/18] chore: add fullstops --- packages/react/src/hooks/usePowerSyncQuery.ts | 4 ++-- packages/react/src/hooks/usePowerSyncWatchedQuery.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/hooks/usePowerSyncQuery.ts b/packages/react/src/hooks/usePowerSyncQuery.ts index 86db3dc9b..84be26a1d 100644 --- a/packages/react/src/hooks/usePowerSyncQuery.ts +++ b/packages/react/src/hooks/usePowerSyncQuery.ts @@ -2,9 +2,9 @@ import React from 'react'; import { usePowerSync } from './PowerSyncContext'; /** - * @deprecated use {@link useQuery} instead + * @deprecated use {@link useQuery} instead. * A hook to access a single static query. - * For an updated result, use {@link usePowerSyncWatchedQuery} instead + * For an updated result, use {@link usePowerSyncWatchedQuery} instead. */ export const usePowerSyncQuery = (sqlStatement: string, parameters: any[] = []): T[] => { const powerSync = usePowerSync(); diff --git a/packages/react/src/hooks/usePowerSyncWatchedQuery.ts b/packages/react/src/hooks/usePowerSyncWatchedQuery.ts index 31c008f8a..14f15b8b9 100644 --- a/packages/react/src/hooks/usePowerSyncWatchedQuery.ts +++ b/packages/react/src/hooks/usePowerSyncWatchedQuery.ts @@ -3,7 +3,7 @@ import React from 'react'; import { usePowerSync } from './PowerSyncContext'; /** - * @deprecated use {@link useQuery} instead + * @deprecated use {@link useQuery} instead. * A hook to access the results of a watched query. * @example * export const Component = () => { From 1cf1669d1e5bb9dc0f140f8daa6e8978d2b9ead4 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Wed, 24 Apr 2024 19:35:14 +0200 Subject: [PATCH 03/18] fix: example --- packages/react/src/hooks/useQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/hooks/useQuery.ts b/packages/react/src/hooks/useQuery.ts index 70415ee58..d2b739ede 100644 --- a/packages/react/src/hooks/useQuery.ts +++ b/packages/react/src/hooks/useQuery.ts @@ -24,7 +24,7 @@ export type WatchedQueryResult = { * @example * ```tsx * export const Component = () => { - * const lists = useQuery('SELECT * from lists'); + * const { data: lists } = useQuery('SELECT * from lists'); * * return * {lists.map((l) => ( From 0f2a3c2b5610ac93d94654acd8aab9f02e59b6a7 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Wed, 24 Apr 2024 20:22:31 +0200 Subject: [PATCH 04/18] fix: example jsdoc --- packages/react/src/hooks/useQuery.ts | 2 -- packages/react/src/hooks/useStatus.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/react/src/hooks/useQuery.ts b/packages/react/src/hooks/useQuery.ts index d2b739ede..1fdcc48ae 100644 --- a/packages/react/src/hooks/useQuery.ts +++ b/packages/react/src/hooks/useQuery.ts @@ -22,7 +22,6 @@ export type WatchedQueryResult = { /** * A hook to access the results of a watched query. * @example - * ```tsx * export const Component = () => { * const { data: lists } = useQuery('SELECT * from lists'); * @@ -32,7 +31,6 @@ export type WatchedQueryResult = { * ))} * * } - * ``` */ export const useQuery = ( sqlStatement: string, diff --git a/packages/react/src/hooks/useStatus.ts b/packages/react/src/hooks/useStatus.ts index 20ffa6dc0..150767942 100644 --- a/packages/react/src/hooks/useStatus.ts +++ b/packages/react/src/hooks/useStatus.ts @@ -4,7 +4,6 @@ import { usePowerSyncStatus } from './usePowerSyncStatus'; * Custom hook that provides access to the current status of PowerSync. * @returns The PowerSync Database status. * @example - * ```JSX * import { useStatus } from "@powersync/react"; * * const Component = () => { @@ -14,6 +13,5 @@ import { usePowerSyncStatus } from './usePowerSyncStatus'; * status.connected ? 'wifi' : 'wifi-off' *
* }; - * ``` */ export const useStatus = usePowerSyncStatus; From 14c51b39b94b65d55592eb7ea3083aabb5e7173b Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Wed, 24 Apr 2024 20:24:57 +0200 Subject: [PATCH 05/18] fix: example jsdoc --- packages/react/src/hooks/usePowerSyncQuery.ts | 1 + packages/react/src/hooks/usePowerSyncStatus.ts | 1 + packages/react/src/hooks/usePowerSyncWatchedQuery.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/react/src/hooks/usePowerSyncQuery.ts b/packages/react/src/hooks/usePowerSyncQuery.ts index 84be26a1d..59feb1d65 100644 --- a/packages/react/src/hooks/usePowerSyncQuery.ts +++ b/packages/react/src/hooks/usePowerSyncQuery.ts @@ -3,6 +3,7 @@ import { usePowerSync } from './PowerSyncContext'; /** * @deprecated use {@link useQuery} instead. + * * A hook to access a single static query. * For an updated result, use {@link usePowerSyncWatchedQuery} instead. */ diff --git a/packages/react/src/hooks/usePowerSyncStatus.ts b/packages/react/src/hooks/usePowerSyncStatus.ts index b565f351a..0798db66a 100644 --- a/packages/react/src/hooks/usePowerSyncStatus.ts +++ b/packages/react/src/hooks/usePowerSyncStatus.ts @@ -3,6 +3,7 @@ import { PowerSyncContext } from './PowerSyncContext'; /** * @deprecated Use {@link useStatus} instead. + * * Custom hook that provides access to the current status of PowerSync. * @returns The PowerSync Database status. * @example diff --git a/packages/react/src/hooks/usePowerSyncWatchedQuery.ts b/packages/react/src/hooks/usePowerSyncWatchedQuery.ts index 14f15b8b9..7521f6b8a 100644 --- a/packages/react/src/hooks/usePowerSyncWatchedQuery.ts +++ b/packages/react/src/hooks/usePowerSyncWatchedQuery.ts @@ -4,6 +4,7 @@ import { usePowerSync } from './PowerSyncContext'; /** * @deprecated use {@link useQuery} instead. + * * A hook to access the results of a watched query. * @example * export const Component = () => { From 9afb5b7030c600408d7437274160489cb9f5a014 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Mon, 29 Apr 2024 09:17:36 +0200 Subject: [PATCH 06/18] chore: add isFetching --- packages/react/src/hooks/useQuery.ts | 25 +++++++++++++++---------- packages/react/tests/useQuery.test.tsx | 6 +++--- packages/react/tsconfig.json | 2 +- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/react/src/hooks/useQuery.ts b/packages/react/src/hooks/useQuery.ts index 1fdcc48ae..d1ae0af87 100644 --- a/packages/react/src/hooks/useQuery.ts +++ b/packages/react/src/hooks/useQuery.ts @@ -12,7 +12,11 @@ export type WatchedQueryResult = { * Indicates the initial loading state (hard loading). Loading becomes false once the first set of results from the watched query is available or an error occurs. */ isLoading: boolean; - error: Error; + /** + * Indicates whether the query is currently fetching data, is true during the initial load and any time when the query is re-evaluating (useful for large queries). + */ + isFetching: boolean; + error: Error | undefined; /** * Function used to run the query again. */ @@ -39,12 +43,13 @@ export const useQuery = ( ): WatchedQueryResult => { const powerSync = usePowerSync(); if (!powerSync) { - return { isLoading: false, data: [], error: new Error('PowerSync not configured.') }; + return { isLoading: false, isFetching: false, data: [], error: new Error('PowerSync not configured.') }; } const [data, setData] = React.useState([]); - const [error, setError] = React.useState(undefined); + const [error, setError] = React.useState(undefined); const [isLoading, setIsLoading] = React.useState(true); + const [isFetching, setIsFetching] = React.useState(true); const memoizedParams = React.useMemo(() => parameters, [...parameters]); const memoizedOptions = React.useMemo(() => options, [JSON.stringify(options)]); @@ -52,12 +57,14 @@ export const useQuery = ( const handleResult = (result: T[]) => { setIsLoading(false); + setIsFetching(false); setData(result); setError(undefined); }; const handleError = (e: Error) => { setIsLoading(false); + setIsFetching(false); setData([]); const wrappedError = new Error('PowerSync failed to fetch data: ' + e.message); wrappedError.cause = e; @@ -65,7 +72,7 @@ export const useQuery = ( }; const fetchData = async () => { - setIsLoading(true); + setIsFetching(true); try { const result = await powerSync.getAll(sqlStatement, parameters); handleResult(result); @@ -82,12 +89,10 @@ export const useQuery = ( if (options.runQueryOnce) { fetchData(); } else { - powerSync.watch( - sqlStatement, - parameters, + powerSync.onChangeWithCallback( { - onResult(results) { - handleResult(results.rows?._array ?? []); + onChange: async () => { + await fetchData(); }, onError(e) { handleError(e); @@ -105,5 +110,5 @@ export const useQuery = ( }; }, [powerSync, sqlStatement, memoizedParams, memoizedOptions]); - return { isLoading, data, error, refresh: fetchData }; + return { isLoading, isFetching, data, error, refresh: fetchData }; }; diff --git a/packages/react/tests/useQuery.test.tsx b/packages/react/tests/useQuery.test.tsx index 43584e270..8d0bbee48 100644 --- a/packages/react/tests/useQuery.test.tsx +++ b/packages/react/tests/useQuery.test.tsx @@ -9,7 +9,7 @@ const mockPowerSync = { registerListener: vi.fn(() => ({ statusChanged: vi.fn(() => 'updated') })), - watch: vi.fn(), + onChangeWithCallback: vi.fn(), getAll: vi.fn(() => ['list1', 'list2']) }; @@ -51,7 +51,7 @@ describe('useQuery', () => { expect(result.current.isLoading).toEqual(false); expect(result.current.data).toEqual(['list1', 'list2']); expect(result.current.isLoading).toEqual(false); - expect(mockPowerSync.watch).not.toHaveBeenCalled(); + expect(mockPowerSync.onChangeWithCallback).not.toHaveBeenCalled(); expect(mockPowerSync.getAll).toHaveBeenCalledTimes(1); }, { timeout: 100 } @@ -101,5 +101,5 @@ describe('useQuery', () => { expect(result.current.error).toEqual(Error('PowerSync failed to fetch data: some error')); }); - // TODO: Add tests for powersync.watch path + // TODO: Add tests for powersync.onChangeWithCallback path }); diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index c3821cad2..c8cbe3756 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -6,7 +6,7 @@ "rootDir": "src", "composite": true, "outDir": "./lib", - "lib": ["esnext", "DOM"], + "lib": ["es2022", "DOM"], "module": "esnext", "sourceMap": true, "moduleResolution": "node", From 8fab13f42ab1c16c2034aa5a17b82ab1603dec97 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Mon, 29 Apr 2024 09:25:45 +0200 Subject: [PATCH 07/18] docs: update readme --- packages/react/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/README.md b/packages/react/README.md index f9d354d7b..c13a6e464 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -58,16 +58,16 @@ const Component = () => { }; ``` -### Watched Queries +### Queries -Watched queries will automatically update when a dependant table is updated. +Queries will automatically update when a dependant table is updated unless you set the `runQueryOnce` flag. ```JSX // TodoListDisplay.jsx -import { usePowerSyncWatchedQuery } from "@powersync/react"; +import { useQuery } from "@powersync/react"; export const TodoListDisplay = () => { - const todoLists = usePowerSyncWatchedQuery('SELECT * from lists'); + const { data: todoLists } = useQuery('SELECT * from lists'); return {todoLists.map((l) => ( From aaf53a868288414a9a6d8c7c3c786f2fbe4758e8 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Mon, 29 Apr 2024 09:32:36 +0200 Subject: [PATCH 08/18] chore: update type name --- packages/react/src/hooks/useQuery.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/hooks/useQuery.ts b/packages/react/src/hooks/useQuery.ts index d1ae0af87..5c7c93aec 100644 --- a/packages/react/src/hooks/useQuery.ts +++ b/packages/react/src/hooks/useQuery.ts @@ -6,7 +6,7 @@ interface AdditionalOptions extends Omit { runQueryOnce?: boolean; } -export type WatchedQueryResult = { +export type QueryResult = { data: T[]; /** * Indicates the initial loading state (hard loading). Loading becomes false once the first set of results from the watched query is available or an error occurs. @@ -40,7 +40,7 @@ export const useQuery = ( sqlStatement: string, parameters: any[] = [], options: AdditionalOptions = {} -): WatchedQueryResult => { +): QueryResult => { const powerSync = usePowerSync(); if (!powerSync) { return { isLoading: false, isFetching: false, data: [], error: new Error('PowerSync not configured.') }; From 2523c8c78662b945b5703ddad471586760576b95 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Mon, 29 Apr 2024 12:41:12 +0200 Subject: [PATCH 09/18] fix: tables issue --- packages/react/src/hooks/useQuery.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/react/src/hooks/useQuery.ts b/packages/react/src/hooks/useQuery.ts index 5c7c93aec..c2d675513 100644 --- a/packages/react/src/hooks/useQuery.ts +++ b/packages/react/src/hooks/useQuery.ts @@ -80,11 +80,19 @@ export const useQuery = ( handleError(e); } }; + const fetchTables = async () => { + const tables = await powerSync.resolveTables(sqlStatement, memoizedParams, memoizedOptions); + return tables; + }; React.useEffect(() => { // Abort any previous watches abortController.current?.abort(); abortController.current = new AbortController(); + let tables = []; + fetchTables().then((t) => { + tables = t; + }); if (options.runQueryOnce) { fetchData(); @@ -100,7 +108,8 @@ export const useQuery = ( }, { ...options, - signal: abortController.current.signal + signal: abortController.current.signal, + tables } ); } From 2454e2b025d0c6adae16fb1afe72b6b0e5d31d54 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Mon, 29 Apr 2024 13:10:18 +0200 Subject: [PATCH 10/18] fix: tests --- packages/react/src/hooks/useQuery.ts | 9 ++++++- packages/react/tests/useQuery.test.tsx | 36 +++++++++++++++----------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/react/src/hooks/useQuery.ts b/packages/react/src/hooks/useQuery.ts index c2d675513..b21fe315a 100644 --- a/packages/react/src/hooks/useQuery.ts +++ b/packages/react/src/hooks/useQuery.ts @@ -80,8 +80,15 @@ export const useQuery = ( handleError(e); } }; + const fetchTables = async () => { - const tables = await powerSync.resolveTables(sqlStatement, memoizedParams, memoizedOptions); + let tables = []; + try { + tables = await powerSync.resolveTables(sqlStatement, memoizedParams, memoizedOptions); + } catch (e) { + handleError(e); + } + return tables; }; diff --git a/packages/react/tests/useQuery.test.tsx b/packages/react/tests/useQuery.test.tsx index 8d0bbee48..fe40af2f0 100644 --- a/packages/react/tests/useQuery.test.tsx +++ b/packages/react/tests/useQuery.test.tsx @@ -9,6 +9,7 @@ const mockPowerSync = { registerListener: vi.fn(() => ({ statusChanged: vi.fn(() => 'updated') })), + resolveTables: vi.fn(() => ['table1', 'table2']), onChangeWithCallback: vi.fn(), getAll: vi.fn(() => ['list1', 'list2']) }; @@ -22,11 +23,12 @@ describe('useQuery', () => { vi.resetAllMocks(); }); - it('should error when PowerSync is not set', () => { + it('should error when PowerSync is not set', async () => { const { result } = renderHook(() => useQuery('SELECT * from lists')); - expect(result.current.error).toEqual(Error('PowerSync not configured.')); - expect(result.current.isLoading).toEqual(false); - expect(result.current.data).toEqual([]); + const currentResult = await result.current; + expect(currentResult.error).toEqual(Error('PowerSync not configured.')); + expect(currentResult.isLoading).toEqual(false); + expect(currentResult.data).toEqual([]); }); it('should set isLoading to true on initial load', async () => { @@ -35,7 +37,8 @@ describe('useQuery', () => { ); const { result } = renderHook(() => useQuery('SELECT * from lists'), { wrapper }); - expect(result.current.isLoading).toEqual(true); + const currentResult = await result.current; + expect(currentResult.isLoading).toEqual(true); }); it('should run the query once if runQueryOnce flag is set', async () => { @@ -44,13 +47,14 @@ describe('useQuery', () => { ); const { result } = renderHook(() => useQuery('SELECT * from lists', [], { runQueryOnce: true }), { wrapper }); - expect(result.current.isLoading).toEqual(true); + const currentResult = await result.current; + expect(currentResult.isLoading).toEqual(true); waitFor( - () => { - expect(result.current.isLoading).toEqual(false); - expect(result.current.data).toEqual(['list1', 'list2']); - expect(result.current.isLoading).toEqual(false); + async () => { + expect(currentResult.isLoading).toEqual(false); + expect(currentResult.data).toEqual(['list1', 'list2']); + expect(currentResult.isLoading).toEqual(false); expect(mockPowerSync.onChangeWithCallback).not.toHaveBeenCalled(); expect(mockPowerSync.getAll).toHaveBeenCalledTimes(1); }, @@ -64,14 +68,15 @@ describe('useQuery', () => { ); const { result } = renderHook(() => useQuery('SELECT * from lists', [], { runQueryOnce: true }), { wrapper }); - expect(result.current.isLoading).toEqual(true); + const currentResult = await result.current; + expect(currentResult.isLoading).toEqual(true); let refresh; waitFor( - () => { - refresh = result.current.refresh; - expect(result.current.isLoading).toEqual(false); + async () => { + refresh = currentResult.refresh; + expect(currentResult.isLoading).toEqual(false); expect(mockPowerSync.getAll).toHaveBeenCalledTimes(1); }, { timeout: 100 } @@ -98,7 +103,8 @@ describe('useQuery', () => { ); const { result } = renderHook(() => useQuery('SELECT * from lists', [], { runQueryOnce: true }), { wrapper }); - expect(result.current.error).toEqual(Error('PowerSync failed to fetch data: some error')); + const currentResult = await result.current; + expect(currentResult.error).toEqual(Error('PowerSync failed to fetch data: some error')); }); // TODO: Add tests for powersync.onChangeWithCallback path From 705966d589b74523c4b70960bca344d704fff13d Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Mon, 29 Apr 2024 13:11:09 +0200 Subject: [PATCH 11/18] docs: update name --- packages/react/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/README.md b/packages/react/README.md index c13a6e464..0bf03f29e 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -44,7 +44,7 @@ export const TodoListDisplay = () => { ### Accessing PowerSync Status -The provided PowerSync client status is available with the `usePowerSyncStatus` hook. +The provided PowerSync client status is available with the `useStatus` hook. ```JSX import { useStatus } from "@powersync/react"; From d4bd09ef7c88a12eed57f29da8a23881526bcdf8 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Mon, 29 Apr 2024 13:44:25 +0200 Subject: [PATCH 12/18] chore: add isLoading examples to demos --- demos/example-nextjs/src/app/page.tsx | 6 +++++- .../app/views/todos/edit/[id].tsx | 8 +++++++- .../src/components/widgets/TodoListsWidget.tsx | 6 +++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/demos/example-nextjs/src/app/page.tsx b/demos/example-nextjs/src/app/page.tsx index d61e47881..a1ce63ff5 100644 --- a/demos/example-nextjs/src/app/page.tsx +++ b/demos/example-nextjs/src/app/page.tsx @@ -6,7 +6,7 @@ import { usePowerSync, useQuery } from '@powersync/react'; export default function EntryPage() { const db = usePowerSync(); - const { data: customers } = useQuery('SELECT id, name FROM customers'); + const { data: customers, isLoading } = useQuery('SELECT id, name FROM customers'); useEffect(() => { // Insert some test data @@ -16,6 +16,10 @@ export default function EntryPage() { return () => {}; }, []); + if (isLoading) { + return ; + } + return ( diff --git a/demos/react-native-supabase-todolist/app/views/todos/edit/[id].tsx b/demos/react-native-supabase-todolist/app/views/todos/edit/[id].tsx index 93b70ce43..c76d57ed4 100644 --- a/demos/react-native-supabase-todolist/app/views/todos/edit/[id].tsx +++ b/demos/react-native-supabase-todolist/app/views/todos/edit/[id].tsx @@ -38,7 +38,7 @@ const TodoView: React.FC = () => { data: [listRecord] } = useQuery<{ name: string }>(`SELECT name FROM ${LIST_TABLE} WHERE id = ?`, [listID]); - const { data: todos } = useQuery( + const { data: todos, isLoading } = useQuery( ` SELECT ${TODO_TABLE}.id AS todo_id, @@ -105,6 +105,12 @@ const TodoView: React.FC = () => { }); }; + if (isLoading) { + + Loading... + ; + } + if (listRecord == null) { return ( diff --git a/demos/react-supabase-todolist/src/components/widgets/TodoListsWidget.tsx b/demos/react-supabase-todolist/src/components/widgets/TodoListsWidget.tsx index 044284fc2..c0a630ff9 100644 --- a/demos/react-supabase-todolist/src/components/widgets/TodoListsWidget.tsx +++ b/demos/react-supabase-todolist/src/components/widgets/TodoListsWidget.tsx @@ -17,7 +17,7 @@ export function TodoListsWidget(props: TodoListsWidgetProps) { const powerSync = usePowerSync(); const navigate = useNavigate(); - const { data: listRecords } = useQuery(` + const { data: listRecords, isLoading } = useQuery(` SELECT ${LISTS_TABLE}.*, COUNT(${TODOS_TABLE}.id) AS total_tasks, SUM(CASE WHEN ${TODOS_TABLE}.completed = true THEN 1 ELSE 0 END) as completed_tasks FROM @@ -37,6 +37,10 @@ export function TodoListsWidget(props: TodoListsWidgetProps) { }); }; + if (isLoading) { + return
Loading...
; + } + return ( {listRecords.map((r) => ( From d0810a70f6d2852733ff3e1f0add05a9f380824a Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Mon, 29 Apr 2024 16:21:08 +0200 Subject: [PATCH 13/18] fix: tables issue --- packages/react/src/hooks/useQuery.ts | 24 ++++++++-------- packages/react/tests/useQuery.test.tsx | 39 ++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/packages/react/src/hooks/useQuery.ts b/packages/react/src/hooks/useQuery.ts index b21fe315a..3c6c3d8e3 100644 --- a/packages/react/src/hooks/useQuery.ts +++ b/packages/react/src/hooks/useQuery.ts @@ -50,6 +50,7 @@ export const useQuery = ( const [error, setError] = React.useState(undefined); const [isLoading, setIsLoading] = React.useState(true); const [isFetching, setIsFetching] = React.useState(true); + const [tables, setTables] = React.useState([]); const memoizedParams = React.useMemo(() => parameters, [...parameters]); const memoizedOptions = React.useMemo(() => options, [JSON.stringify(options)]); @@ -82,28 +83,27 @@ export const useQuery = ( }; const fetchTables = async () => { - let tables = []; try { - tables = await powerSync.resolveTables(sqlStatement, memoizedParams, memoizedOptions); + const tables = await powerSync.resolveTables(sqlStatement, memoizedParams, memoizedOptions); + setTables(tables); } catch (e) { handleError(e); } - - return tables; }; + React.useEffect(() => { + (async () => { + await fetchTables(); + await fetchData(); + })(); + }, []); + React.useEffect(() => { // Abort any previous watches abortController.current?.abort(); abortController.current = new AbortController(); - let tables = []; - fetchTables().then((t) => { - tables = t; - }); - if (options.runQueryOnce) { - fetchData(); - } else { + if (!options.runQueryOnce) { powerSync.onChangeWithCallback( { onChange: async () => { @@ -124,7 +124,7 @@ export const useQuery = ( return () => { abortController.current?.abort(); }; - }, [powerSync, sqlStatement, memoizedParams, memoizedOptions]); + }, [powerSync, sqlStatement, memoizedParams, memoizedOptions, tables]); return { isLoading, isFetching, data, error, refresh: fetchData }; }; diff --git a/packages/react/tests/useQuery.test.tsx b/packages/react/tests/useQuery.test.tsx index fe40af2f0..7206b4680 100644 --- a/packages/react/tests/useQuery.test.tsx +++ b/packages/react/tests/useQuery.test.tsx @@ -92,7 +92,8 @@ describe('useQuery', () => { registerListener: vi.fn(() => ({ statusChanged: vi.fn(() => 'updated') })), - watch: vi.fn(), + onChangeWithCallback: vi.fn(), + resolveTables: vi.fn(() => ['table1', 'table2']), getAll: vi.fn(() => { throw new Error('some error'); }) @@ -104,7 +105,41 @@ describe('useQuery', () => { const { result } = renderHook(() => useQuery('SELECT * from lists', [], { runQueryOnce: true }), { wrapper }); const currentResult = await result.current; - expect(currentResult.error).toEqual(Error('PowerSync failed to fetch data: some error')); + + waitFor( + async () => { + expect(currentResult.error).toEqual(Error('PowerSync failed to fetch data: some error')); + }, + { timeout: 100 } + ); + }); + + it('should set error when error occurs', async () => { + const mockPowerSyncError = { + currentStatus: { status: 'initial' }, + registerListener: vi.fn(() => ({ + statusChanged: vi.fn(() => 'updated') + })), + onChangeWithCallback: vi.fn(), + resolveTables: vi.fn(() => ['table1', 'table2']), + getAll: vi.fn(() => { + throw new Error('some error'); + }) + }; + + const wrapper = ({ children }) => ( + {children} + ); + + const { result } = renderHook(() => useQuery('SELECT * from lists', []), { wrapper }); + const currentResult = await result.current; + + waitFor( + async () => { + expect(currentResult.error).toEqual(Error('PowerSync failed to fetch data: some error')); + }, + { timeout: 100 } + ); }); // TODO: Add tests for powersync.onChangeWithCallback path From 8167a0f1595ca82eed56066d874be526bbbd874e Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Tue, 30 Apr 2024 09:41:37 +0200 Subject: [PATCH 14/18] fix: test --- packages/react/tests/useStatus.test.tsx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/react/tests/useStatus.test.tsx b/packages/react/tests/useStatus.test.tsx index a0727661a..2fd221f29 100644 --- a/packages/react/tests/useStatus.test.tsx +++ b/packages/react/tests/useStatus.test.tsx @@ -4,11 +4,11 @@ import { vi, describe, expect, it, afterEach } from 'vitest'; import { useStatus } from '../src/hooks/useStatus'; import { PowerSyncContext } from '../src/hooks/PowerSyncContext'; +const callback = vi.fn(); + const mockPowerSync = { currentStatus: { status: 'initial' }, - registerListener: vi.fn(() => ({ - statusChanged: vi.fn(() => 'updated') - })) + registerListener: () => callback }; vi.mock('./PowerSyncContext', () => ({ @@ -31,17 +31,22 @@ describe('useStatus', () => { // TODO: Get this test to work it.skip('should update the status when the listener is called', () => { + const mockPowerSyncInTest = { + currentStatus: { status: 'initial' }, + registerListener: () => ({ + statusChanged: () => 'updated' + }) + }; + const wrapper = ({ children }) => ( - {children} + {children} ); const { result } = renderHook(() => useStatus(), { wrapper }); act(() => { - mockPowerSync.registerListener.mockResolvedValue({ statusChanged: vi.fn(() => 'updated') }); + expect(result.current).toEqual({ status: 'updated' }); }); - - expect(result.current).toEqual({ status: 'updated' }); }); it('should run the listener on unmount', () => { @@ -50,7 +55,7 @@ describe('useStatus', () => { ); const { unmount } = renderHook(() => useStatus(), { wrapper }); - const listenerUnsubscribe = mockPowerSync.registerListener; + const listenerUnsubscribe = callback; unmount(); From 133b77823f6e86f553509bfdac915f2d88ad57d2 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Tue, 30 Apr 2024 10:36:09 +0200 Subject: [PATCH 15/18] chore: add hasSynced examples --- .../library/widgets/HeaderWidget.tsx | 3 +- .../src/app/(app)/contacts/index.tsx | 2 +- .../app/views/todos/lists.tsx | 35 +++++++++++-------- .../src/app/views/todo-lists/page.tsx | 5 +-- .../src/app/editor/page.tsx | 28 +++++++++------ .../src/app/page.tsx | 2 +- 6 files changed, 44 insertions(+), 31 deletions(-) diff --git a/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx b/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx index f139be2a5..0dff32a29 100644 --- a/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx +++ b/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx @@ -39,7 +39,8 @@ export const HeaderWidget: React.FC<{ onPress={() => { Alert.alert( 'Status', - `${status.connected ? 'Connected' : 'Disconnected'}. \nLast Synced at ${status.lastSyncedAt?.toISOString() ?? '-' + `${status.connected ? 'Connected' : 'Disconnected'}. \nLast Synced at ${ + status.lastSyncedAt?.toISOString() ?? '-' }\nVersion: ${powersync.sdkVersion}` ); }} diff --git a/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx b/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx index 6d476a3ca..9cdee5ff0 100644 --- a/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx +++ b/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx @@ -112,7 +112,7 @@ export default function ContactsIndex() { icon={} backgroundColor="$brand1" borderRadius="$3" - // circular + // circular /> diff --git a/demos/react-native-supabase-todolist/app/views/todos/lists.tsx b/demos/react-native-supabase-todolist/app/views/todos/lists.tsx index 19bc3d53c..bd60a3bc8 100644 --- a/demos/react-native-supabase-todolist/app/views/todos/lists.tsx +++ b/demos/react-native-supabase-todolist/app/views/todos/lists.tsx @@ -7,7 +7,7 @@ import prompt from 'react-native-prompt-android'; import { router, Stack } from 'expo-router'; import { LIST_TABLE, TODO_TABLE, ListRecord } from '../../../library/powersync/AppSchema'; import { useSystem } from '../../../library/powersync/system'; -import { useQuery } from '@powersync/react-native'; +import { useQuery, useStatus } from '@powersync/react-native'; import { ListItemWidget } from '../../../library/widgets/ListItemWidget'; const description = (total: number, completed: number = 0) => { @@ -16,6 +16,7 @@ const description = (total: number, completed: number = 0) => { const ListsViewWidget: React.FC = () => { const system = useSystem(); + const status = useStatus(); const { data: listRecords } = useQuery(` SELECT ${LIST_TABLE}.*, COUNT(${TODO_TABLE}.id) AS total_tasks, SUM(CASE WHEN ${TODO_TABLE}.completed = true THEN 1 ELSE 0 END) as completed_tasks @@ -77,20 +78,24 @@ const ListsViewWidget: React.FC = () => { }} /> - {listRecords.map((r) => ( - deleteList(r.id)} - onPress={() => { - router.push({ - pathname: 'views/todos/edit/[id]', - params: { id: r.id } - }); - }} - /> - ))} + {!status.hasSynced ? ( +

Busy with sync...

+ ) : ( + listRecords.map((r) => ( + deleteList(r.id)} + onPress={() => { + router.push({ + pathname: 'views/todos/edit/[id]', + params: { id: r.id } + }); + }} + /> + )) + )}
diff --git a/demos/react-supabase-todolist/src/app/views/todo-lists/page.tsx b/demos/react-supabase-todolist/src/app/views/todo-lists/page.tsx index adedb355d..086c1d406 100644 --- a/demos/react-supabase-todolist/src/app/views/todo-lists/page.tsx +++ b/demos/react-supabase-todolist/src/app/views/todo-lists/page.tsx @@ -1,4 +1,4 @@ -import { usePowerSync } from '@powersync/react'; +import { usePowerSync, useStatus } from '@powersync/react'; import AddIcon from '@mui/icons-material/Add'; import { Box, @@ -22,6 +22,7 @@ import { SearchBarWidget } from '@/components/widgets/SearchBarWidget'; export default function TodoListsPage() { const powerSync = usePowerSync(); const supabase = useSupabase(); + const status = useStatus(); const [showPrompt, setShowPrompt] = React.useState(false); const nameInputRef = React.createRef(); @@ -52,7 +53,7 @@ export default function TodoListsPage() { - + {!status.hasSynced ?

Busy with sync...

: }
{/* TODO use a dialog service in future, this is just a simple example app */} -
- {editor && } - -
- - - {totalDocUpdates} total edit(s) in this document. - - + {!status.hasSynced ? ( +

Busy with sync...

+ ) : ( + <> +
+ {editor && } + +
+ + + {totalDocUpdates} total edit(s) in this document. + + + + )} ); } diff --git a/demos/yjs-react-supabase-text-collab/src/app/page.tsx b/demos/yjs-react-supabase-text-collab/src/app/page.tsx index 2626c5b30..e7d4e4308 100644 --- a/demos/yjs-react-supabase-text-collab/src/app/page.tsx +++ b/demos/yjs-react-supabase-text-collab/src/app/page.tsx @@ -21,7 +21,7 @@ export default function EntryPage() { return; } // otherwise, create a new document - const { data, error } = await connector.client + const { data } = await connector.client .from('documents') .insert({ title: 'Test Document ' + (1000 + Math.floor(Math.random() * 8999)) From 92a70d83de9b77137c8555c4d205e3407cf5ac0a Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Tue, 30 Apr 2024 10:38:38 +0200 Subject: [PATCH 16/18] docs: update readme --- packages/react/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react/README.md b/packages/react/README.md index 0bf03f29e..bf9ddabfe 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -52,9 +52,12 @@ import { useStatus } from "@powersync/react"; const Component = () => { const status = useStatus(); - return
- status.connected ? 'wifi' : 'wifi-off' -
+ return ( + <> +
status.connected ? 'wifi' : 'wifi-off'
+
!status.hasSynced ? 'Busy syncing...' : 'Data is here'
+ + ) }; ``` From e24f1ecdfb6adba72a96d40ad0f26f603b53613d Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Tue, 30 Apr 2024 10:51:25 +0200 Subject: [PATCH 17/18] fix: example --- packages/react/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/README.md b/packages/react/README.md index bf9ddabfe..3f54b4e8e 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -54,8 +54,8 @@ const Component = () => { return ( <> -
status.connected ? 'wifi' : 'wifi-off'
-
!status.hasSynced ? 'Busy syncing...' : 'Data is here'
+
{status.connected} ? 'wifi' : 'wifi-off'
+
{!status.hasSynced} ? 'Busy syncing...' : 'Data is here'
) }; From 5e99651d7bc932298842e6f9700f1120eae00f6b Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Tue, 30 Apr 2024 11:25:59 +0200 Subject: [PATCH 18/18] docs: update readme --- packages/react/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/README.md b/packages/react/README.md index 3f54b4e8e..b2e9096b6 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -54,8 +54,8 @@ const Component = () => { return ( <> -
{status.connected} ? 'wifi' : 'wifi-off'
-
{!status.hasSynced} ? 'Busy syncing...' : 'Data is here'
+
{status.connected ? 'wifi' : 'wifi-off'}
+
{!status.hasSynced ? 'Busy syncing...' : 'Data is here'}
) };