Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slow-melons-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/react': patch
---

Refactor useQuery hook to avoid calling internal hooks conditionally.
42 changes: 21 additions & 21 deletions packages/react/src/hooks/watched/useQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,26 +61,26 @@ export function useQuery<RowType = any>(
return { isLoading: false, isFetching: false, data: [], error: new Error('PowerSync not configured.') };
}
const { parsedQuery, queryChanged } = constructCompatibleQuery(query, parameters, options);
const runOnce = options?.runQueryOnce == true;
const single = useSingleQuery<RowType>({
query: parsedQuery,
powerSync,
queryChanged,
active: runOnce
});
const watched = useWatchedQuery<RowType>({
query: parsedQuery,
powerSync,
queryChanged,
options: {
reportFetching: options.reportFetching,
// Maintains backwards compatibility with previous versions
// Differentiation is opt-in by default
// We emit new data for each table change by default.
rowComparator: options.rowComparator
},
active: !runOnce
});

switch (options?.runQueryOnce) {
case true:
return useSingleQuery<RowType>({
query: parsedQuery,
powerSync,
queryChanged
});
default:
return useWatchedQuery<RowType>({
query: parsedQuery,
powerSync,
queryChanged,
options: {
reportFetching: options.reportFetching,
// Maintains backwards compatibility with previous versions
// Differentiation is opt-in by default
// We emit new data for each table change by default.
rowComparator: options.rowComparator
}
});
}
return runOnce ? single : watched;
}
20 changes: 13 additions & 7 deletions packages/react/src/hooks/watched/useSingleQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import React from 'react';
import { QueryResult } from './watch-types.js';
import { InternalHookOptions } from './watch-utils.js';

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

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

// Trigger initial query execution
// @ts-ignore: Complains about not all code paths returning a value
React.useEffect(() => {
const abortController = new AbortController();
runQuery(abortController.signal);
return () => {
abortController.abort();
};
}, [powerSync, queryChanged]);
if (active) {
const abortController = new AbortController();
runQuery(abortController.signal);
return () => {
abortController.abort();
};
}
}, [powerSync, active, queryChanged]);

return {
...output,
Expand Down
20 changes: 12 additions & 8 deletions packages/react/src/hooks/watched/useWatchedQuery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { useWatchedQuerySubscription } from './useWatchedQuerySubscription.js';
import { useNullableWatchedQuerySubscription } from './useWatchedQuerySubscription.js';
import { DifferentialHookOptions, QueryResult, ReadonlyQueryResult } from './watch-types.js';
import { InternalHookOptions } from './watch-utils.js';

Expand All @@ -14,9 +14,13 @@ import { InternalHookOptions } from './watch-utils.js';
export const useWatchedQuery = <RowType = unknown>(
options: InternalHookOptions<RowType[]> & { options: DifferentialHookOptions<RowType> }
): QueryResult<RowType> | ReadonlyQueryResult<RowType> => {
const { query, powerSync, queryChanged, options: hookOptions } = options;
const { query, powerSync, queryChanged, options: hookOptions, active } = options;

function createWatchedQuery() {
if (!active) {
return null;
}

const createWatchedQuery = React.useCallback(() => {
const watch = hookOptions.rowComparator
? powerSync.customQuery(query).differentialWatch({
rowComparator: hookOptions.rowComparator,
Expand All @@ -28,26 +32,26 @@ export const useWatchedQuery = <RowType = unknown>(
throttleMs: hookOptions.throttleMs
});
return watch;
}, []);
}

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

React.useEffect(() => {
watchedQuery.close();
watchedQuery?.close();
setWatchedQuery(createWatchedQuery);
}, [powerSync]);
}, [powerSync, active]);

// Indicates that the query will be re-fetched due to a change in the query.
// Used when `isFetching` hasn't been set to true yet due to React execution.
React.useEffect(() => {
if (queryChanged) {
watchedQuery.updateSettings({
watchedQuery?.updateSettings({
query,
throttleMs: hookOptions.throttleMs,
reportFetching: hookOptions.reportFetching
});
}
}, [queryChanged]);

return useWatchedQuerySubscription(watchedQuery);
return useNullableWatchedQuerySubscription(watchedQuery);
};
31 changes: 22 additions & 9 deletions packages/react/src/hooks/watched/useWatchedQuerySubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,31 @@ export const useWatchedQuerySubscription = <
>(
query: Query
): Query['state'] => {
const [output, setOutputState] = React.useState(query.state);
return useNullableWatchedQuerySubscription(query);
};

/**
* @internal
*/
export const useNullableWatchedQuerySubscription = <
ResultType = unknown,
Query extends WatchedQuery<ResultType> = WatchedQuery<ResultType>
>(
query: Query | null
): Query['state'] | undefined => {
const [output, setOutputState] = React.useState(query?.state);

// @ts-ignore: Complains about not all code paths returning a value
React.useEffect(() => {
const dispose = query.registerListener({
onStateChange: (state) => {
setOutputState({ ...state });
}
});
if (query) {
setOutputState(query.state);

return () => {
dispose();
};
return query.registerListener({
onStateChange: (state) => {
setOutputState({ ...state });
}
});
}
}, [query]);

return output;
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/hooks/watched/watch-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type InternalHookOptions<DataType> = {
query: WatchCompatibleQuery<DataType>;
powerSync: AbstractPowerSyncDatabase;
queryChanged: boolean;
active: boolean;
};

export const checkQueryChanged = <T>(query: WatchCompatibleQuery<T>, options: AdditionalOptions) => {
Expand Down
47 changes: 47 additions & 0 deletions packages/react/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,53 @@ describe('useQuery', () => {
expect(result.current.data == previousData).false;
});

it('should be able to switch between single and watched query', async () => {
const db = openPowerSync();
const wrapper = ({ children }) => <PowerSyncContext.Provider value={db}>{children}</PowerSyncContext.Provider>;

let changeRunOnce: React.Dispatch<React.SetStateAction<boolean>>;
const { result } = renderHook(
() => {
const [runOnce, setRunOnce] = React.useState(true);
changeRunOnce = setRunOnce;

return useQuery('SELECT * FROM lists WHERE name = ?', ['aname'], { runQueryOnce: runOnce });
},
{ wrapper }
);

// Wait for the query to run once.
await waitFor(
async () => {
const { current } = result;
expect(current.isLoading).toEqual(false);
},
{ timeout: 500, interval: 100 }
);

// Then switch to watched queries.
act(() => changeRunOnce(false));
expect(result.current.isLoading).toBeTruthy();

await waitFor(
async () => {
const { current } = result;
expect(current.isLoading).toEqual(false);
},
{ timeout: 500, interval: 100 }
);

// Because we're watching, this should trigger an update.
await db.execute('INSERT INTO lists(id, name) VALUES (uuid(), ?)', ['aname']);
await waitFor(
async () => {
const { current } = result;
expect(current.data.length).toEqual(1);
},
{ timeout: 500, interval: 100 }
);
});

it('should use an existing WatchedQuery instance', async () => {
const db = openPowerSync();

Expand Down