Skip to content

Commit 6d63019

Browse files
committed
feat(tanstack-react-query): add useQueries hook support
1 parent 71db1e7 commit 6d63019

File tree

2 files changed

+633
-0
lines changed

2 files changed

+633
-0
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { type CompilableQuery, parseQuery } from '@powersync/common';
2+
import { usePowerSync } from '@powersync/react';
3+
import * as Tanstack from '@tanstack/react-query';
4+
import { useEffect, useMemo, useState } from 'react';
5+
6+
export type PowerSyncQueryOptions<T> = {
7+
query?: string | CompilableQuery<T>;
8+
parameters?: any[];
9+
};
10+
11+
export type UseBaseQueryOptions<TQueryOptions> = TQueryOptions & PowerSyncQueryOptions<any>;
12+
13+
export type PowerSyncQueriesOptions<T extends any[]> = {
14+
[K in keyof T]: Tanstack.UseQueryOptions<T[K], any> & PowerSyncQueryOptions<T[K]>;
15+
};
16+
17+
type PowerSyncQueryOption<T = any> = Tanstack.UseQueryOptions<T[], any> & PowerSyncQueryOptions<T>;
18+
19+
type InferQueryResults<TQueries extends readonly any[]> = {
20+
[K in keyof TQueries]: TQueries[K] extends { query: CompilableQuery<infer TData> }
21+
? Tanstack.UseQueryResult<TData[], any>
22+
: Tanstack.UseQueryResult<unknown[], any>;
23+
};
24+
25+
type ExplicitQueryResults<T extends readonly any[]> = {
26+
[K in keyof T]: Tanstack.UseQueryResult<T[K][], any>;
27+
};
28+
29+
type EnhancedInferQueryResults<TQueries extends readonly any[]> = {
30+
[K in keyof TQueries]: TQueries[K] extends { query: CompilableQuery<infer TData> }
31+
? Tanstack.UseQueryResult<TData[], any> & { queryKey: Tanstack.QueryKey }
32+
: Tanstack.UseQueryResult<unknown[], any> & { queryKey: Tanstack.QueryKey };
33+
};
34+
35+
type EnhancedExplicitQueryResults<T extends readonly any[]> = {
36+
[K in keyof T]: Tanstack.UseQueryResult<T[K][], any> & { queryKey: Tanstack.QueryKey };
37+
};
38+
39+
// Explicit generic typing with combine
40+
export function useQueries<T extends readonly any[], TCombined>(
41+
options: {
42+
queries: readonly [...{ [K in keyof T]: PowerSyncQueryOption<T[K]> }];
43+
combine: (results: EnhancedExplicitQueryResults<T>) => TCombined;
44+
},
45+
queryClient?: Tanstack.QueryClient
46+
): TCombined;
47+
48+
// Explicit generic typing without combine
49+
export function useQueries<T extends readonly any[]>(
50+
options: {
51+
queries: readonly [...{ [K in keyof T]: PowerSyncQueryOption<T[K]> }];
52+
combine?: undefined;
53+
},
54+
queryClient?: Tanstack.QueryClient
55+
): ExplicitQueryResults<T>;
56+
57+
// Auto inference with combine
58+
export function useQueries<TQueries extends readonly PowerSyncQueryOption[], TCombined>(
59+
options: {
60+
queries: readonly [...TQueries];
61+
combine: (results: EnhancedInferQueryResults<TQueries>) => TCombined;
62+
},
63+
queryClient?: Tanstack.QueryClient
64+
): TCombined;
65+
66+
// Auto inference without combine
67+
export function useQueries<TQueries extends readonly PowerSyncQueryOption[]>(
68+
options: {
69+
queries: readonly [...TQueries];
70+
combine?: undefined;
71+
},
72+
queryClient?: Tanstack.QueryClient
73+
): InferQueryResults<TQueries>;
74+
75+
// Implementation
76+
export function useQueries(
77+
options: {
78+
queries: readonly (Tanstack.UseQueryOptions<any, any> & PowerSyncQueryOptions<any>)[];
79+
combine?: (results: (Tanstack.UseQueryResult<any, any> & { queryKey: Tanstack.QueryKey })[]) => any;
80+
},
81+
queryClient: Tanstack.QueryClient = Tanstack.useQueryClient()
82+
) {
83+
const powerSync = usePowerSync();
84+
const queriesInput = options.queries;
85+
const [tablesArr, setTablesArr] = useState<string[][]>(() => queriesInput.map(() => []));
86+
const [errorsArr, setErrorsArr] = useState<(Error | undefined)[]>(() => queriesInput.map(() => undefined));
87+
88+
const parsedQueries = useMemo(() => {
89+
return queriesInput.map((queryOptions: Tanstack.UseQueryOptions<any, any> & PowerSyncQueryOptions<any>) => {
90+
const { query, parameters = [], ...rest } = queryOptions;
91+
let sqlStatement = '';
92+
let queryParameters: any[] = [];
93+
let error: Error | undefined = undefined;
94+
if (query) {
95+
try {
96+
const parsedQuery = parseQuery(query, parameters);
97+
sqlStatement = parsedQuery.sqlStatement;
98+
queryParameters = parsedQuery.parameters;
99+
} catch (e) {
100+
error = e as Error;
101+
}
102+
}
103+
return { query, parameters, rest, sqlStatement, queryParameters, error };
104+
});
105+
}, [queriesInput]);
106+
107+
useEffect(() => {
108+
const listeners: (undefined | (() => void))[] = [];
109+
parsedQueries.forEach((q, idx) => {
110+
if (q.error || !q.query) return;
111+
(async () => {
112+
try {
113+
const t = await powerSync.resolveTables(q.sqlStatement, q.queryParameters);
114+
setTablesArr((prev) => {
115+
if (JSON.stringify(prev[idx]) === JSON.stringify(t)) return prev;
116+
const next = prev.slice();
117+
next[idx] = t;
118+
return next;
119+
});
120+
} catch (e) {
121+
setErrorsArr((prev) => {
122+
if (prev[idx]?.message === (e as Error).message) return prev;
123+
const next = prev.slice();
124+
next[idx] = e as Error;
125+
return next;
126+
});
127+
}
128+
})();
129+
const l = powerSync.registerListener({
130+
schemaChanged: async () => {
131+
try {
132+
const t = await powerSync.resolveTables(q.sqlStatement, q.queryParameters);
133+
setTablesArr((prev) => {
134+
if (JSON.stringify(prev[idx]) === JSON.stringify(t)) return prev;
135+
const next = prev.slice();
136+
next[idx] = t;
137+
return next;
138+
});
139+
queryClient.invalidateQueries({ queryKey: q.rest.queryKey });
140+
} catch (e) {
141+
setErrorsArr((prev) => {
142+
if (prev[idx]?.message === (e as Error).message) return prev;
143+
const next = prev.slice();
144+
next[idx] = e as Error;
145+
return next;
146+
});
147+
}
148+
},
149+
});
150+
listeners[idx] = l;
151+
});
152+
153+
return () => {
154+
listeners.forEach((l) => l?.());
155+
};
156+
}, [powerSync, parsedQueries, queryClient]);
157+
158+
useEffect(() => {
159+
const aborts: AbortController[] = [];
160+
parsedQueries.forEach((q, idx) => {
161+
if (q.error || !q.query) return;
162+
const abort = new AbortController();
163+
aborts[idx] = abort;
164+
powerSync.onChangeWithCallback(
165+
{
166+
onChange: () => {
167+
queryClient.invalidateQueries({ queryKey: q.rest.queryKey });
168+
},
169+
onError: (e) => {
170+
setErrorsArr((prev) => {
171+
if (prev[idx]?.message === (e as Error).message) return prev;
172+
const next = prev.slice();
173+
next[idx] = e as Error;
174+
return next;
175+
});
176+
},
177+
},
178+
{
179+
tables: tablesArr[idx],
180+
signal: abort.signal,
181+
}
182+
);
183+
});
184+
return () => aborts.forEach((a) => a?.abort());
185+
}, [powerSync, parsedQueries, queryClient, tablesArr]);
186+
187+
const queries = useMemo(() => {
188+
return parsedQueries.map((q, idx) => {
189+
const error = q.error || errorsArr[idx];
190+
const queryFn = async () => {
191+
if (error) throw error;
192+
try {
193+
return typeof q.query === 'string'
194+
? powerSync.getAll(q.sqlStatement, q.queryParameters)
195+
: q.query?.execute();
196+
} catch (e) {
197+
throw e;
198+
}
199+
};
200+
return {
201+
...q.rest,
202+
queryFn: q.query ? queryFn : q.rest.queryFn,
203+
queryKey: q.rest.queryKey,
204+
};
205+
});
206+
}, [parsedQueries, errorsArr, powerSync]);
207+
208+
return Tanstack.useQueries(
209+
{
210+
queries: queries as Tanstack.QueriesOptions<any>,
211+
combine: options.combine
212+
? (results) => {
213+
const enhancedResults = results.map((result, index) => ({
214+
...result,
215+
queryKey: queries[index].queryKey,
216+
}));
217+
218+
return options.combine?.(enhancedResults);
219+
}
220+
: undefined,
221+
},
222+
queryClient
223+
);
224+
}

0 commit comments

Comments
 (0)