Skip to content

Commit d88fc5f

Browse files
feat: Tanstack useQueries support (#642)
Co-authored-by: Christiaan Landman <chriz.ek@gmail.com> Co-authored-by: Christiaan Landman <christiaan@journeyapps.com>
1 parent 6e6db2a commit d88fc5f

File tree

8 files changed

+930
-108
lines changed

8 files changed

+930
-108
lines changed

.changeset/tall-dodos-watch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/tanstack-react-query': patch
3+
---
4+
5+
Added Tanstack useQueries support. See https://tanstack.com/query/latest/docs/framework/react/reference/useQueries for more information.

packages/tanstack-react-query/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,38 @@ export const TodoListDisplaySuspense = () => {
106106
};
107107
```
108108
109+
### useQueries
110+
111+
The `useQueries` hook allows you to run multiple queries in parallel and combine the results into a single result.
112+
113+
```JSX
114+
// TodoListDisplay.jsx
115+
import { useQueries } from '@powersync/tanstack-react-query';
116+
117+
export const TodoListDisplay = () => {
118+
const { data: todoLists } = useQueries({
119+
queries: [
120+
{ queryKey: ['todoLists'], query: 'SELECT * from lists' },
121+
{ queryKey: ['todoLists2'], query: 'SELECT * from lists2' },
122+
],
123+
combine: (results) => {
124+
return {
125+
data: results.map((result) => result.data),
126+
pending: results.some((result) => result.isPending),
127+
}
128+
},
129+
});
130+
131+
return (
132+
<div>
133+
{todoLists.map((list) => (
134+
<li key={list.id}>{list.name}</li>
135+
))}
136+
</div>
137+
);
138+
};
139+
```
140+
109141
### TypeScript Support
110142
111143
A type can be specified for each row returned by `useQuery` and `useSuspenseQuery`.
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { type CompilableQuery, parseQuery } from '@powersync/common';
2+
import { usePowerSync } from '@powersync/react';
3+
import { useEffect, useState, useCallback, useMemo } from 'react';
4+
import * as Tanstack from '@tanstack/react-query';
5+
6+
export type UsePowerSyncQueriesInput = {
7+
query?: string | CompilableQuery<unknown>;
8+
parameters?: unknown[];
9+
queryKey: Tanstack.QueryKey;
10+
}[];
11+
12+
export type UsePowerSyncQueriesOutput = {
13+
sqlStatement: string;
14+
queryParameters: unknown[];
15+
tables: string[];
16+
error?: Error;
17+
queryFn: () => Promise<unknown[]>;
18+
}[];
19+
20+
export function usePowerSyncQueries(
21+
queries: UsePowerSyncQueriesInput,
22+
queryClient: Tanstack.QueryClient
23+
): UsePowerSyncQueriesOutput {
24+
const powerSync = usePowerSync();
25+
26+
const [tablesArr, setTablesArr] = useState<string[][]>(() => queries.map(() => []));
27+
const [errorsArr, setErrorsArr] = useState<(Error | undefined)[]>(() => queries.map(() => undefined));
28+
29+
const updateTablesArr = useCallback((tables: string[], idx: number) => {
30+
setTablesArr((prev) => {
31+
if (JSON.stringify(prev[idx]) === JSON.stringify(tables)) return prev;
32+
const next = [...prev];
33+
next[idx] = tables;
34+
return next;
35+
});
36+
}, []);
37+
38+
const updateErrorsArr = useCallback((error: Error | undefined, idx: number) => {
39+
setErrorsArr((prev) => {
40+
if (prev[idx]?.message === error?.message) return prev;
41+
const next = [...prev];
42+
next[idx] = error;
43+
return next;
44+
});
45+
}, []);
46+
47+
const parsedQueries = useMemo(
48+
() =>
49+
queries.map((queryInput) => {
50+
const { query, parameters = [], queryKey } = queryInput;
51+
52+
if (!query) {
53+
return {
54+
query,
55+
parameters,
56+
queryKey,
57+
sqlStatement: '',
58+
queryParameters: [],
59+
parseError: undefined
60+
};
61+
}
62+
63+
try {
64+
const parsed = parseQuery(query, parameters);
65+
return {
66+
query,
67+
parameters,
68+
queryKey,
69+
sqlStatement: parsed.sqlStatement,
70+
queryParameters: parsed.parameters,
71+
parseError: undefined
72+
};
73+
} catch (e) {
74+
return {
75+
query,
76+
parameters,
77+
queryKey,
78+
sqlStatement: '',
79+
queryParameters: [],
80+
parseError: e as Error
81+
};
82+
}
83+
}),
84+
[queries]
85+
);
86+
87+
useEffect(() => {
88+
parsedQueries.forEach((pq, idx) => {
89+
if (pq.parseError) {
90+
updateErrorsArr(pq.parseError, idx);
91+
}
92+
});
93+
}, [parsedQueries, updateErrorsArr]);
94+
95+
const stringifiedQueriesDeps = JSON.stringify(
96+
parsedQueries.map((q) => ({
97+
sql: q.sqlStatement,
98+
params: q.queryParameters
99+
}))
100+
);
101+
102+
useEffect(() => {
103+
const listeners = parsedQueries.map((pq, idx) => {
104+
if (pq.parseError || !pq.query) {
105+
return null;
106+
}
107+
108+
(async () => {
109+
try {
110+
const tables = await powerSync.resolveTables(pq.sqlStatement, pq.queryParameters);
111+
updateTablesArr(tables, idx);
112+
} catch (e) {
113+
updateErrorsArr(e as Error, idx);
114+
}
115+
})();
116+
117+
return powerSync.registerListener({
118+
schemaChanged: async () => {
119+
try {
120+
const tables = await powerSync.resolveTables(pq.sqlStatement, pq.queryParameters);
121+
updateTablesArr(tables, idx);
122+
queryClient.invalidateQueries({ queryKey: pq.queryKey });
123+
} catch (e) {
124+
updateErrorsArr(e as Error, idx);
125+
}
126+
}
127+
});
128+
});
129+
130+
return () => {
131+
listeners.forEach((l) => l?.());
132+
};
133+
}, [powerSync, queryClient, stringifiedQueriesDeps, updateTablesArr, updateErrorsArr]);
134+
135+
const stringifiedQueryKeys = JSON.stringify(parsedQueries.map((q) => q.queryKey));
136+
137+
useEffect(() => {
138+
const aborts = parsedQueries.map((pq, idx) => {
139+
if (pq.parseError || !pq.query) {
140+
return null;
141+
}
142+
143+
const abort = new AbortController();
144+
145+
powerSync.onChangeWithCallback(
146+
{
147+
onChange: () => {
148+
queryClient.invalidateQueries({ queryKey: pq.queryKey });
149+
},
150+
onError: (e) => {
151+
updateErrorsArr(e, idx);
152+
}
153+
},
154+
{
155+
tables: tablesArr[idx],
156+
signal: abort.signal
157+
}
158+
);
159+
160+
return abort;
161+
});
162+
163+
return () => aborts.forEach((a) => a?.abort());
164+
}, [powerSync, queryClient, tablesArr, updateErrorsArr, stringifiedQueryKeys]);
165+
166+
return useMemo(() => {
167+
return parsedQueries.map((pq, idx) => {
168+
const error = errorsArr[idx] || pq.parseError;
169+
170+
const queryFn = async () => {
171+
if (error) throw error;
172+
if (!pq.query) throw new Error('No query provided');
173+
174+
try {
175+
return typeof pq.query === 'string'
176+
? await powerSync.getAll(pq.sqlStatement, pq.queryParameters)
177+
: await pq.query.execute();
178+
} catch (e) {
179+
throw e;
180+
}
181+
};
182+
183+
return {
184+
sqlStatement: pq.sqlStatement,
185+
queryParameters: pq.queryParameters,
186+
tables: tablesArr[idx],
187+
error,
188+
queryFn
189+
};
190+
});
191+
}, [parsedQueries, errorsArr, tablesArr, powerSync]);
192+
}

0 commit comments

Comments
 (0)