Skip to content

Commit 4c5b136

Browse files
Add tests for shared queries
1 parent 719d396 commit 4c5b136

File tree

6 files changed

+147
-5
lines changed

6 files changed

+147
-5
lines changed

packages/react-native/rollup.config.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const __filename = fileURLToPath(import.meta.url);
1212
const __dirname = path.dirname(__filename);
1313

1414
export default (commandLineArgs) => {
15-
const sourcemap = (commandLineArgs.sourceMap || 'true') == 'true';
15+
const sourceMap = (commandLineArgs.sourceMap || 'true') == 'true';
1616

1717
// Clears rollup CLI warning https://github.com/rollup/rollup/issues/2694
1818
delete commandLineArgs.sourceMap;
@@ -22,7 +22,7 @@ export default (commandLineArgs) => {
2222
output: {
2323
file: 'dist/index.js',
2424
format: 'cjs',
25-
sourcemap: sourcemap
25+
sourcemap: sourceMap
2626
},
2727
plugins: [
2828
// We do this so that we can inject on BSON's crypto usage.
@@ -54,7 +54,7 @@ export default (commandLineArgs) => {
5454
}
5555
]
5656
}),
57-
terser()
57+
terser({ sourceMap })
5858
],
5959
external: [
6060
'@journeyapps/react-native-quick-sqlite',

packages/react/src/hooks/watched/useWatchedQuery.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ export const useWatchedQuery = <RowType = unknown>(
4545
// Used when `isFetching` hasn't been set to true yet due to React execution.
4646
React.useEffect(() => {
4747
if (queryChanged) {
48-
console.log('Query changed, re-fetching...');
4948
watchedQuery.updateSettings({
5049
placeholderData: [],
5150
query,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { WatchedQuery, WatchedQueryState } from '@powersync/common';
2+
import React from 'react';
3+
4+
/**
5+
* A hook to access and subscribe to the results of an existing {@link WatchedQuery} instance.
6+
* @example
7+
* export const ContentComponent = () => {
8+
* const { data: lists } = useWatchedQuerySuspenseSubscription(listsQuery);
9+
*
10+
* return <View>
11+
* {lists.map((l) => (
12+
* <Text key={l.id}>{JSON.stringify(l)}</Text>
13+
* ))}
14+
* </View>;
15+
* }
16+
*
17+
*/
18+
export const useWatchedQuerySubscription = <ResultType = unknown>(
19+
query: WatchedQuery<ResultType>
20+
): WatchedQueryState<ResultType> => {
21+
const [output, setOutputState] = React.useState(query.state);
22+
23+
React.useEffect(() => {
24+
const dispose = query.subscribe({
25+
onStateChange: (state) => {
26+
setOutputState({ ...state });
27+
}
28+
});
29+
30+
return () => {
31+
dispose();
32+
};
33+
}, [query]);
34+
35+
return output;
36+
};

packages/react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export { useSuspenseQuery } from './hooks/suspense/useSuspenseQuery';
77
export { useWatchedQuerySuspenseSubscription } from './hooks/suspense/useWatchedQuerySuspenseSubscription';
88
export { useStatus } from './hooks/useStatus';
99
export { useQuery } from './hooks/watched/useQuery';
10+
export { useWatchedQuerySubscription } from './hooks/watched/useWatchedQuerySubscription';
1011
export { AdditionalOptions } from './hooks/watched/watch-types';

packages/react/tests/useQuery.test.tsx

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import pDefer from 'p-defer';
66
import { beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest';
77
import { PowerSyncContext } from '../src/hooks/PowerSyncContext';
88
import { useQuery } from '../src/hooks/watched/useQuery';
9+
import { useWatchedQuerySubscription } from '../src/hooks/watched/useWatchedQuerySubscription';
910
export const openPowerSync = () => {
1011
const db = new PowerSyncDatabase({
1112
database: { dbFilename: 'test.db' },
@@ -224,7 +225,7 @@ describe('useQuery', () => {
224225
const deferred = pDefer();
225226

226227
const baseGetAll = db.getAll;
227-
const getSpy = vi.spyOn(db, 'getAll').mockImplementation(async (sql, params) => {
228+
vi.spyOn(db, 'getAll').mockImplementation(async (sql, params) => {
228229
// Allow pausing this call in order to test isFetching
229230
await deferred.promise;
230231
return baseGetAll.call(db, sql, params);
@@ -255,5 +256,56 @@ describe('useQuery', () => {
255256
expect(data == result.current.data).toEqual(true);
256257
});
257258

259+
it('should use an existing WatchedQuery instance', async () => {
260+
const db = openPowerSync();
261+
262+
// This query can be instantiated once and reused.
263+
// The query retains it's state and will not re-fetch the data unless the result changes.
264+
// This is useful for queries that are used in multiple components.
265+
const listsQuery = db.incrementalWatch({
266+
watch: {
267+
placeholderData: [],
268+
query: {
269+
compile: () => ({
270+
sql: `SELECT * FROM lists`,
271+
parameters: []
272+
}),
273+
execute: ({ sql, parameters }) => db.getAll(sql, parameters)
274+
}
275+
}
276+
});
277+
278+
const wrapper = ({ children }) => <PowerSyncContext.Provider value={db}>{children}</PowerSyncContext.Provider>;
279+
const { result } = renderHook(() => useWatchedQuerySubscription(listsQuery), {
280+
wrapper
281+
});
282+
283+
expect(result.current.isLoading).toEqual(true);
284+
285+
await waitFor(
286+
async () => {
287+
const { current } = result;
288+
expect(current.isLoading).toEqual(false);
289+
},
290+
{ timeout: 500, interval: 100 }
291+
);
292+
293+
// This should trigger an update
294+
await db.execute('INSERT INTO lists(id, name) VALUES (uuid(), ?)', ['aname']);
295+
296+
await waitFor(
297+
async () => {
298+
const { current } = result;
299+
expect(current.data.length).toEqual(1);
300+
},
301+
{ timeout: 500, interval: 100 }
302+
);
303+
304+
// now use the same query again, the result should be available immediately
305+
const { result: newResult } = renderHook(() => useWatchedQuerySubscription(listsQuery), { wrapper });
306+
expect(newResult.current.isLoading).toEqual(false);
307+
expect(newResult.current.data.length).toEqual(1);
308+
});
309+
258310
// TODO: Add tests for powersync.onChangeWithCallback path
259311
});

packages/react/tests/useSuspenseQuery.test.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ErrorBoundary } from 'react-error-boundary';
55
import { beforeEach, describe, expect, it, vi } from 'vitest';
66
import { PowerSyncContext } from '../src/hooks/PowerSyncContext';
77
import { useSuspenseQuery } from '../src/hooks/suspense/useSuspenseQuery';
8+
import { useWatchedQuerySuspenseSubscription } from '../src/hooks/suspense/useWatchedQuerySuspenseSubscription';
89
import { openPowerSync } from './useQuery.test';
910

1011
describe('useSuspenseQuery', () => {
@@ -242,4 +243,57 @@ describe('useSuspenseQuery', () => {
242243
await waitForCompletedSuspend();
243244
await waitForError();
244245
});
246+
247+
it('should use an existing WatchedQuery instance', async () => {
248+
const db = openPowerSync();
249+
250+
// This query can be instantiated once and reused.
251+
// The query retains it's state and will not re-fetch the data unless the result changes.
252+
// This is useful for queries that are used in multiple components.
253+
const listsQuery = db.incrementalWatch({
254+
watch: {
255+
placeholderData: [],
256+
query: {
257+
compile: () => ({
258+
sql: `SELECT * FROM lists`,
259+
parameters: []
260+
}),
261+
execute: ({ sql, parameters }) => db.getAll(sql, parameters)
262+
}
263+
}
264+
});
265+
266+
const wrapper = ({ children }) => <PowerSyncContext.Provider value={db}>{children}</PowerSyncContext.Provider>;
267+
const { result } = renderHook(() => useWatchedQuerySuspenseSubscription(listsQuery), {
268+
wrapper
269+
});
270+
271+
// Initially, the query should be loading/suspended
272+
expect(result.current).toEqual(null);
273+
274+
await waitFor(
275+
async () => {
276+
expect(result.current).not.null;
277+
},
278+
{ timeout: 500, interval: 100 }
279+
);
280+
281+
expect(result.current.data.length).toEqual(0);
282+
283+
// This should trigger an update
284+
await db.execute('INSERT INTO lists(id, name) VALUES (uuid(), ?)', ['aname']);
285+
286+
await waitFor(
287+
async () => {
288+
const { current } = result;
289+
expect(current.data.length).toEqual(1);
290+
},
291+
{ timeout: 500, interval: 100 }
292+
);
293+
294+
// now use the same query again, the result should be available immediately
295+
const { result: newResult } = renderHook(() => useWatchedQuerySuspenseSubscription(listsQuery), { wrapper });
296+
expect(newResult.current).not.null;
297+
expect(newResult.current.data.length).toEqual(1);
298+
});
245299
});

0 commit comments

Comments
 (0)