Skip to content

Commit 8decd49

Browse files
committed
Avoid calling hooks conditionally
1 parent c910c66 commit 8decd49

File tree

7 files changed

+121
-45
lines changed

7 files changed

+121
-45
lines changed

.changeset/slow-melons-raise.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/react': patch
3+
---
4+
5+
Refactor useQuery hook to avoid calling internal hooks conditionally.

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

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,26 +61,26 @@ export function useQuery<RowType = any>(
6161
return { isLoading: false, isFetching: false, data: [], error: new Error('PowerSync not configured.') };
6262
}
6363
const { parsedQuery, queryChanged } = constructCompatibleQuery(query, parameters, options);
64+
const runOnce = options?.runQueryOnce == true;
65+
const single = useSingleQuery<RowType>({
66+
query: parsedQuery,
67+
powerSync,
68+
queryChanged,
69+
active: runOnce
70+
});
71+
const watched = useWatchedQuery<RowType>({
72+
query: parsedQuery,
73+
powerSync,
74+
queryChanged,
75+
options: {
76+
reportFetching: options.reportFetching,
77+
// Maintains backwards compatibility with previous versions
78+
// Differentiation is opt-in by default
79+
// We emit new data for each table change by default.
80+
rowComparator: options.rowComparator
81+
},
82+
active: !runOnce
83+
});
6484

65-
switch (options?.runQueryOnce) {
66-
case true:
67-
return useSingleQuery<RowType>({
68-
query: parsedQuery,
69-
powerSync,
70-
queryChanged
71-
});
72-
default:
73-
return useWatchedQuery<RowType>({
74-
query: parsedQuery,
75-
powerSync,
76-
queryChanged,
77-
options: {
78-
reportFetching: options.reportFetching,
79-
// Maintains backwards compatibility with previous versions
80-
// Differentiation is opt-in by default
81-
// We emit new data for each table change by default.
82-
rowComparator: options.rowComparator
83-
}
84-
});
85-
}
85+
return runOnce ? single : watched;
8686
}

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import React from 'react';
22
import { QueryResult } from './watch-types.js';
33
import { InternalHookOptions } from './watch-utils.js';
44

5+
/**
6+
* @internal not exported from `index.ts`
7+
*/
58
export const useSingleQuery = <RowType = any>(options: InternalHookOptions<RowType[]>): QueryResult<RowType> => {
6-
const { query, powerSync, queryChanged } = options;
9+
const { query, powerSync, queryChanged, active } = options;
710

811
const [output, setOutputState] = React.useState<QueryResult<RowType>>({
912
isLoading: true,
@@ -46,13 +49,16 @@ export const useSingleQuery = <RowType = any>(options: InternalHookOptions<RowTy
4649
);
4750

4851
// Trigger initial query execution
52+
// @ts-ignore: Complains about not all code paths returning a value
4953
React.useEffect(() => {
50-
const abortController = new AbortController();
51-
runQuery(abortController.signal);
52-
return () => {
53-
abortController.abort();
54-
};
55-
}, [powerSync, queryChanged]);
54+
if (active) {
55+
const abortController = new AbortController();
56+
runQuery(abortController.signal);
57+
return () => {
58+
abortController.abort();
59+
};
60+
}
61+
}, [powerSync, active, queryChanged]);
5662

5763
return {
5864
...output,

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { useWatchedQuerySubscription } from './useWatchedQuerySubscription.js';
2+
import { useNullableWatchedQuerySubscription } from './useWatchedQuerySubscription.js';
33
import { DifferentialHookOptions, QueryResult, ReadonlyQueryResult } from './watch-types.js';
44
import { InternalHookOptions } from './watch-utils.js';
55

@@ -14,9 +14,13 @@ import { InternalHookOptions } from './watch-utils.js';
1414
export const useWatchedQuery = <RowType = unknown>(
1515
options: InternalHookOptions<RowType[]> & { options: DifferentialHookOptions<RowType> }
1616
): QueryResult<RowType> | ReadonlyQueryResult<RowType> => {
17-
const { query, powerSync, queryChanged, options: hookOptions } = options;
17+
const { query, powerSync, queryChanged, options: hookOptions, active } = options;
18+
19+
function createWatchedQuery() {
20+
if (!active) {
21+
return null;
22+
}
1823

19-
const createWatchedQuery = React.useCallback(() => {
2024
const watch = hookOptions.rowComparator
2125
? powerSync.customQuery(query).differentialWatch({
2226
rowComparator: hookOptions.rowComparator,
@@ -28,26 +32,26 @@ export const useWatchedQuery = <RowType = unknown>(
2832
throttleMs: hookOptions.throttleMs
2933
});
3034
return watch;
31-
}, []);
35+
}
3236

3337
const [watchedQuery, setWatchedQuery] = React.useState(createWatchedQuery);
3438

3539
React.useEffect(() => {
36-
watchedQuery.close();
40+
watchedQuery?.close();
3741
setWatchedQuery(createWatchedQuery);
38-
}, [powerSync]);
42+
}, [powerSync, active]);
3943

4044
// Indicates that the query will be re-fetched due to a change in the query.
4145
// Used when `isFetching` hasn't been set to true yet due to React execution.
4246
React.useEffect(() => {
4347
if (queryChanged) {
44-
watchedQuery.updateSettings({
48+
watchedQuery?.updateSettings({
4549
query,
4650
throttleMs: hookOptions.throttleMs,
4751
reportFetching: hookOptions.reportFetching
4852
});
4953
}
5054
}, [queryChanged]);
5155

52-
return useWatchedQuerySubscription(watchedQuery);
56+
return useNullableWatchedQuerySubscription(watchedQuery);
5357
};

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

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,31 @@ export const useWatchedQuerySubscription = <
2121
>(
2222
query: Query
2323
): Query['state'] => {
24-
const [output, setOutputState] = React.useState(query.state);
24+
return useNullableWatchedQuerySubscription(query);
25+
};
26+
27+
/**
28+
* @internal
29+
*/
30+
export const useNullableWatchedQuerySubscription = <
31+
ResultType = unknown,
32+
Query extends WatchedQuery<ResultType> = WatchedQuery<ResultType>
33+
>(
34+
query: Query | null
35+
): Query['state'] | undefined => {
36+
const [output, setOutputState] = React.useState(query?.state);
2537

38+
// @ts-ignore: Complains about not all code paths returning a value
2639
React.useEffect(() => {
27-
const dispose = query.registerListener({
28-
onStateChange: (state) => {
29-
setOutputState({ ...state });
30-
}
31-
});
40+
if (query) {
41+
setOutputState(query.state);
3242

33-
return () => {
34-
dispose();
35-
};
43+
return query.registerListener({
44+
onStateChange: (state) => {
45+
setOutputState({ ...state });
46+
}
47+
});
48+
}
3649
}, [query]);
3750

3851
return output;

packages/react/src/hooks/watched/watch-utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type InternalHookOptions<DataType> = {
77
query: WatchCompatibleQuery<DataType>;
88
powerSync: AbstractPowerSyncDatabase;
99
queryChanged: boolean;
10+
active: boolean;
1011
};
1112

1213
export const checkQueryChanged = <T>(query: WatchCompatibleQuery<T>, options: AdditionalOptions) => {

packages/react/tests/useQuery.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,53 @@ describe('useQuery', () => {
309309
expect(result.current.data == previousData).false;
310310
});
311311

312+
it('should be able to switch between single and watched query', async () => {
313+
const db = openPowerSync();
314+
const wrapper = ({ children }) => <PowerSyncContext.Provider value={db}>{children}</PowerSyncContext.Provider>;
315+
316+
let changeRunOnce: React.Dispatch<React.SetStateAction<boolean>>;
317+
const { result } = renderHook(
318+
() => {
319+
const [runOnce, setRunOnce] = React.useState(true);
320+
changeRunOnce = setRunOnce;
321+
322+
return useQuery('SELECT * FROM lists WHERE name = ?', ['aname'], { runQueryOnce: runOnce });
323+
},
324+
{ wrapper }
325+
);
326+
327+
// Wait for the query to run once.
328+
await waitFor(
329+
async () => {
330+
const { current } = result;
331+
expect(current.isLoading).toEqual(false);
332+
},
333+
{ timeout: 500, interval: 100 }
334+
);
335+
336+
// Then switch to watched queries.
337+
act(() => changeRunOnce(false));
338+
expect(result.current.isLoading).toBeTruthy();
339+
340+
await waitFor(
341+
async () => {
342+
const { current } = result;
343+
expect(current.isLoading).toEqual(false);
344+
},
345+
{ timeout: 500, interval: 100 }
346+
);
347+
348+
// Because we're watching, this should trigger an update.
349+
await db.execute('INSERT INTO lists(id, name) VALUES (uuid(), ?)', ['aname']);
350+
await waitFor(
351+
async () => {
352+
const { current } = result;
353+
expect(current.data.length).toEqual(1);
354+
},
355+
{ timeout: 500, interval: 100 }
356+
);
357+
});
358+
312359
it('should use an existing WatchedQuery instance', async () => {
313360
const db = openPowerSync();
314361

0 commit comments

Comments
 (0)