Skip to content

Commit 385edf8

Browse files
DominicGBauerDominicGBauer
andauthored
feat(react): add usePowerSyncStatus hook (#137)
Co-authored-by: DominicGBauer <dominic@nomanini.com>
1 parent 93d9e7d commit 385edf8

File tree

17 files changed

+1553
-1680
lines changed

17 files changed

+1553
-1680
lines changed

.changeset/blue-cameras-flash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@journeyapps/powersync-react": minor
3+
---
4+
5+
Add `usePowerSyncStatus` hook

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ _Bad connectivity is everywhere, and we're tired of it. PowerSync is on a missio
4040
Demo applications are located in the [`demos/`](./demos/) directory. Also see our [Demo Apps / Example Projects](https://docs.powersync.com/resources/demo-apps-example-projects) gallery which lists all projects by the backend and client-side framework they use.
4141

4242
### React Native
43+
4344
- [demos/react-native-supabase-todolist](./demos/react-native-supabase-todolist): A React Native to-do list example app using a Supabase backend.
4445
- [demos/django-react-native-todolist](./demos/django-react-native-todolist) A React Native to-do list example app using a Django backend.
4546

4647
### Web
48+
4749
- [demos/react-supabase-todolist](./demos/react-supabase-todolist/README.md): A React to-do list example app using the PowerSync Web SDK and a Supabase backend.
4850
- [demos/yjs-react-supabase-text-collab](./demos/yjs-react-supabase-text-collab/README.md): A React real-time text editing collaboration example app powered by [Yjs](https://github.com/yjs/yjs) CRDTs and [Tiptap](https://tiptap.dev/), using the PowerSync Web SDK and a Supabase backend.
4951
- [demos/vue-supabase-todolist](./demos/vue-supabase-todolist/README.md): A Vue to-do list example app using the PowerSync Web SDK and a Supabase backend.

demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Alert, Text } from 'react-native';
33
import { Icon } from 'react-native-elements';
44
import { useNavigation } from 'expo-router';
55
import { useSystem } from '../stores/system';
6+
import { usePowerSyncStatus } from '@journeyapps/powersync-react';
67
import { Header } from 'react-native-elements';
78
import { observer } from 'mobx-react-lite';
89
import { DrawerActions } from '@react-navigation/native';
@@ -12,7 +13,9 @@ export const HeaderWidget: React.FC<{
1213
}> = observer((props) => {
1314
const { title } = props;
1415
const { powersync } = useSystem();
16+
const status = usePowerSyncStatus();
1517
const navigation = useNavigation();
18+
1619
return (
1720
<Header
1821
leftComponent={
@@ -28,16 +31,15 @@ export const HeaderWidget: React.FC<{
2831
}
2932
rightComponent={
3033
<Icon
31-
name={powersync.connected ? 'wifi' : 'wifi-off'}
34+
name={status.connected ? 'wifi' : 'wifi-off'}
3235
type="material-community"
3336
color="black"
3437
size={20}
3538
style={{ padding: 5 }}
3639
onPress={() => {
3740
Alert.alert(
3841
'Status',
39-
`${powersync.connected ? 'Connected' : 'Disconnected'}. \nLast Synced at ${
40-
powersync.currentStatus?.lastSyncedAt.toISOString() ?? '-'
42+
`${status.connected ? 'Connected' : 'Disconnected'}. \nLast Synced at ${status.lastSyncedAt?.toISOString() ?? '-'
4143
}\nVersion: ${powersync.sdkVersion}`
4244
);
4345
}}

demos/react-native-supabase-todolist/library/widgets/HeaderWidget.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Alert, Text } from 'react-native';
33
import { Icon } from 'react-native-elements';
44
import { useNavigation } from 'expo-router';
55
import { Header } from 'react-native-elements';
6+
import { usePowerSyncStatus } from '@journeyapps/powersync-react';
67
import { DrawerActions } from '@react-navigation/native';
78
import { useSystem } from '../powersync/system';
89

@@ -12,15 +13,7 @@ export const HeaderWidget: React.FC<{
1213
const system = useSystem();
1314
const { powersync } = system;
1415
const navigation = useNavigation();
15-
const [connected, setConnected] = React.useState(powersync.connected);
16-
17-
React.useEffect(() => {
18-
return powersync.registerListener({
19-
statusChanged: (status) => {
20-
setConnected(status.connected);
21-
}
22-
});
23-
}, [powersync]);
16+
const status = usePowerSyncStatus();
2417

2518
const { title } = props;
2619
return (
@@ -38,7 +31,7 @@ export const HeaderWidget: React.FC<{
3831
}
3932
rightComponent={
4033
<Icon
41-
name={connected ? 'wifi' : 'wifi-off'}
34+
name={status.connected ? 'wifi' : 'wifi-off'}
4235
type="material-community"
4336
color="black"
4437
size={20}
@@ -47,8 +40,7 @@ export const HeaderWidget: React.FC<{
4740
system.attachmentQueue.trigger();
4841
Alert.alert(
4942
'Status',
50-
`${connected ? 'Connected' : 'Disconnected'}. \nLast Synced at ${
51-
powersync.currentStatus?.lastSyncedAt.toISOString() ?? '-'
43+
`${status.connected ? 'Connected' : 'Disconnected'}. \nLast Synced at ${status?.lastSyncedAt?.toISOString() ?? '-'
5244
}\nVersion: ${powersync.sdkVersion}`
5345
);
5446
}}

demos/react-supabase-todolist/src/app/views/layout.tsx

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@ import React from 'react';
2525

2626
import { useNavigationPanel } from '@/components/navigation/NavigationPanelContext';
2727
import { useSupabase } from '@/components/providers/SystemProvider';
28-
import { usePowerSync } from '@journeyapps/powersync-react';
28+
import { usePowerSync, usePowerSyncStatus } from '@journeyapps/powersync-react';
2929
import { useNavigate } from 'react-router-dom';
3030
import { LOGIN_ROUTE, SQL_CONSOLE_ROUTE, TODO_LISTS_ROUTE } from '@/app/router';
3131

3232
export default function ViewsLayout({ children }: { children: React.ReactNode }) {
3333
const powerSync = usePowerSync();
34+
const status = usePowerSyncStatus();
3435
const supabase = useSupabase();
3536
const navigate = useNavigate();
3637

37-
const [syncStatus, setSyncStatus] = React.useState(powerSync.currentStatus);
3838
const [openDrawer, setOpenDrawer] = React.useState(false);
3939
const { title } = useNavigationPanel();
4040

@@ -63,15 +63,6 @@ export default function ViewsLayout({ children }: { children: React.ReactNode })
6363
[powerSync, supabase]
6464
);
6565

66-
React.useEffect(() => {
67-
const l = powerSync.registerListener({
68-
statusChanged: (status) => {
69-
setSyncStatus(status);
70-
}
71-
});
72-
return () => l?.();
73-
}, [powerSync]);
74-
7566
return (
7667
<S.MainBox>
7768
<S.TopBar position="static">
@@ -89,12 +80,9 @@ export default function ViewsLayout({ children }: { children: React.ReactNode })
8980
<Box sx={{ flexGrow: 1 }}>
9081
<Typography>{title}</Typography>
9182
</Box>
92-
<NorthIcon
93-
sx={{ marginRight: '-10px' }}
94-
color={syncStatus?.dataFlowStatus.uploading ? 'primary' : 'inherit'}
95-
/>
96-
<SouthIcon color={syncStatus?.dataFlowStatus.downloading ? 'primary' : 'inherit'} />
97-
{syncStatus?.connected ? <WifiIcon /> : <SignalWifiOffIcon />}
83+
<NorthIcon sx={{ marginRight: '-10px' }} color={status?.dataFlowStatus.uploading ? 'primary' : 'inherit'} />
84+
<SouthIcon color={status?.dataFlowStatus.downloading ? 'primary' : 'inherit'} />
85+
{status?.connected ? <WifiIcon /> : <SignalWifiOffIcon />}
9886
</Toolbar>
9987
</S.TopBar>
10088
<Drawer anchor={'left'} open={openDrawer} onClose={() => setOpenDrawer(false)}>

packages/powersync-react/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Configure a PowerSync DB connection and add it to a context provider.
88
// App.jsx
99
import { PowerSyncDatabase } from '@journeyapps/powersync-react-native';
1010
import { PowerSyncContext } from "@journeyapps/powersync-react";
11+
1112
export const App = () => {
1213
const powerSync = React.useMemo(() => {
1314
// Setup PowerSync client
@@ -42,6 +43,22 @@ export const TodoListDisplay = () => {
4243
}
4344
```
4445

46+
### Accessing PowerSync Status
47+
48+
The provided PowerSync client status is available with the `usePowerSyncStatus` hook.
49+
50+
```JSX
51+
import { usePowerSyncStatus } from "@journeyapps/powersync-react";
52+
53+
const Component = () => {
54+
const status = usePowerSyncStatus();
55+
56+
return <div>
57+
status.connected ? 'wifi' : 'wifi-off'
58+
</div>
59+
};
60+
```
61+
4562
### Watched Queries
4663

4764
Watched queries will automatically update when a dependant table is updated.

packages/powersync-react/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"scripts": {
1515
"build": "tsc -b",
1616
"clean": "rm -rf lib tsconfig.tsbuildinfo",
17+
"test": "vitest",
1718
"watch": "tsc -b -w"
1819
},
1920
"repository": {
@@ -33,7 +34,10 @@
3334
"react": "*"
3435
},
3536
"devDependencies": {
37+
"@testing-library/react": "^15.0.2",
38+
"@testing-library/react-hooks": "^8.0.1",
3639
"@types/react": "^18.2.34",
40+
"jsdom": "^24.0.0",
3741
"react": "18.2.0",
3842
"typescript": "^5.1.3"
3943
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useContext, useEffect, useState } from 'react';
2+
import { PowerSyncContext } from './PowerSyncContext';
3+
4+
/**
5+
* Custom hook that provides access to the current status of PowerSync.
6+
* @returns The PowerSync Database status.
7+
* @example
8+
* const Component = () => {
9+
* const status = usePowerSyncStatus();
10+
*
11+
* return <div>
12+
* status.connected ? 'wifi' : 'wifi-off'
13+
* </div>
14+
* };
15+
*/
16+
export const usePowerSyncStatus = () => {
17+
const powerSync = useContext(PowerSyncContext);
18+
const [syncStatus, setSyncStatus] = useState(powerSync.currentStatus);
19+
20+
useEffect(() => {
21+
const listener = powerSync.registerListener({
22+
statusChanged: (status) => {
23+
setSyncStatus(status);
24+
}
25+
});
26+
27+
return () => listener?.();
28+
}, [powerSync]);
29+
30+
return syncStatus;
31+
};
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './hooks/PowerSyncContext';
2-
export * from './hooks/usePowerSyncQuery';
3-
export * from './hooks/usePowerSyncWatchedQuery';
2+
export { usePowerSyncQuery } from './hooks/usePowerSyncQuery';
3+
export { usePowerSyncWatchedQuery } from './hooks/usePowerSyncWatchedQuery';
4+
export { usePowerSyncStatus } from './hooks/usePowerSyncStatus';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import { describe, expect, it } from 'vitest';
4+
import * as SUT from '../src/hooks/PowerSyncContext';
5+
6+
describe('PowerSyncContext', () => {
7+
describe('usePowerSync', () => {
8+
it('should retrieve the PowerSync DB from the context and display the test text', async () => {
9+
const TestComponent = () => {
10+
const powerSyncDb = SUT.usePowerSync() as any;
11+
return <div>{powerSyncDb.testText}</div>;
12+
};
13+
14+
const { findByText } = render(
15+
<SUT.PowerSyncContext.Provider value={{ testText: 'Test Text' } as any}>
16+
<TestComponent />
17+
</SUT.PowerSyncContext.Provider>
18+
);
19+
const hello = await findByText('Test Text');
20+
21+
expect(hello).toBeDefined;
22+
});
23+
});
24+
});

0 commit comments

Comments
 (0)