Skip to content

Commit 25ba01b

Browse files
committed
feat(tanstack-react-query): extract common PowerSync query logic to eliminate duplication between useQueries & useQuery
1 parent b8cbf30 commit 25ba01b

File tree

4 files changed

+229
-214
lines changed

4 files changed

+229
-214
lines changed
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+
}

packages/tanstack-react-query/src/hooks/useQueries.ts

Lines changed: 23 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { type CompilableQuery, parseQuery } from '@powersync/common';
1+
import { type CompilableQuery } from '@powersync/common';
22
import { usePowerSync } from '@powersync/react';
33
import * as Tanstack from '@tanstack/react-query';
4-
import { useEffect, useMemo, useState, useCallback } from 'react';
4+
import { useMemo } from 'react';
5+
import { usePowerSyncQueries } from './usePowerSyncQueries';
56

67
export type PowerSyncQueryOptions<T> = {
78
query?: string | CompilableQuery<T>;
@@ -75,145 +76,37 @@ export function useQueries(
7576
queryClient: Tanstack.QueryClient = Tanstack.useQueryClient()
7677
) {
7778
const powerSync = usePowerSync();
78-
const queriesInput = options.queries;
79-
const [tablesArr, setTablesArr] = useState<string[][]>(() => queriesInput.map(() => []));
80-
const [errorsArr, setErrorsArr] = useState<(Error | undefined)[]>(() => queriesInput.map(() => undefined));
81-
82-
const updateTablesArr = useCallback((tables: string[], idx: number) => {
83-
setTablesArr((prev) => {
84-
if (JSON.stringify(prev[idx]) === JSON.stringify(tables)) return prev;
85-
const next = [...prev];
86-
next[idx] = tables;
87-
return next;
88-
});
89-
}, []);
90-
91-
const updateErrorsArr = useCallback((error: Error, idx: number) => {
92-
setErrorsArr((prev) => {
93-
if (prev[idx]?.message === error.message) return prev;
94-
const next = [...prev];
95-
next[idx] = error;
96-
return next;
97-
});
98-
}, []);
9979

100-
const parsedQueries = useMemo(
101-
() =>
102-
queriesInput.map((queryOptions) => {
103-
const { query, parameters = [], ...rest } = queryOptions;
104-
const parsed = (() => {
105-
if (!query) {
106-
return { sqlStatement: '', queryParameters: [], error: undefined };
107-
}
80+
if (!powerSync) {
81+
throw new Error('PowerSync is not available');
82+
}
10883

109-
try {
110-
const parsedQuery = parseQuery(query, parameters);
111-
return {
112-
sqlStatement: parsedQuery.sqlStatement,
113-
queryParameters: parsedQuery.parameters,
114-
error: undefined
115-
};
116-
} catch (e) {
117-
return {
118-
sqlStatement: '',
119-
queryParameters: [],
120-
error: e as Error
121-
};
122-
}
123-
})();
84+
const queriesInput = options.queries;
12485

125-
return { query, parameters, rest, ...parsed };
126-
}),
86+
const powerSyncQueriesInput = useMemo(
87+
() =>
88+
queriesInput.map((queryOptions) => ({
89+
query: queryOptions.query,
90+
parameters: queryOptions.parameters,
91+
queryKey: queryOptions.queryKey
92+
})),
12793
[queriesInput]
12894
);
12995

130-
const stringifiedQueriesDeps = JSON.stringify(
131-
parsedQueries.map((q) => ({
132-
sql: q.sqlStatement,
133-
params: q.queryParameters
134-
}))
135-
);
136-
137-
useEffect(() => {
138-
const listeners = parsedQueries.map((q, idx) => {
139-
if (q.error || !q.query) {
140-
return null;
141-
}
142-
143-
(async () => {
144-
try {
145-
const t = await powerSync.resolveTables(q.sqlStatement, q.queryParameters);
146-
updateTablesArr(t, idx);
147-
} catch (e) {
148-
updateErrorsArr(e, idx);
149-
}
150-
})();
151-
return powerSync.registerListener({
152-
schemaChanged: async () => {
153-
try {
154-
const t = await powerSync.resolveTables(q.sqlStatement, q.queryParameters);
155-
updateTablesArr(t, idx);
156-
queryClient.invalidateQueries({ queryKey: q.rest.queryKey });
157-
} catch (e) {
158-
updateErrorsArr(e, idx);
159-
}
160-
}
161-
});
162-
});
163-
164-
return () => {
165-
listeners.forEach((l) => l?.());
166-
};
167-
}, [powerSync, queryClient, stringifiedQueriesDeps, updateErrorsArr, updateTablesArr]);
168-
169-
const stringifiedQueryKeys = JSON.stringify(parsedQueries.map((q) => q.rest.queryKey));
170-
171-
useEffect(() => {
172-
const aborts = parsedQueries.map((q, idx) => {
173-
if (q.error || !q.query) {
174-
return null;
175-
}
176-
177-
const abort = new AbortController();
178-
179-
powerSync.onChangeWithCallback(
180-
{
181-
onChange: () => {
182-
queryClient.invalidateQueries({ queryKey: q.rest.queryKey });
183-
},
184-
onError: (e) => {
185-
updateErrorsArr(e, idx);
186-
}
187-
},
188-
{
189-
tables: tablesArr[idx],
190-
signal: abort.signal
191-
}
192-
);
193-
return abort;
194-
});
195-
return () => aborts.forEach((a) => a?.abort());
196-
}, [powerSync, queryClient, tablesArr, updateErrorsArr, stringifiedQueryKeys]);
96+
const states = usePowerSyncQueries(powerSyncQueriesInput, queryClient);
19797

19898
const queries = useMemo(() => {
199-
return parsedQueries.map((q, idx) => {
200-
const error = q.error || errorsArr[idx];
201-
const queryFn = async () => {
202-
if (error) throw error;
203-
204-
try {
205-
return typeof q.query === 'string' ? powerSync.getAll(q.sqlStatement, q.queryParameters) : q.query?.execute();
206-
} catch (e) {
207-
throw e;
208-
}
209-
};
99+
return queriesInput.map((queryOptions, idx) => {
100+
const { query, parameters, ...rest } = queryOptions;
101+
const state = states[idx];
102+
210103
return {
211-
...q.rest,
212-
queryFn: q.query ? queryFn : q.rest.queryFn,
213-
queryKey: q.rest.queryKey
104+
...rest,
105+
queryFn: query ? state.queryFn : rest.queryFn,
106+
queryKey: rest.queryKey
214107
};
215108
});
216-
}, [stringifiedQueriesDeps, errorsArr]);
109+
}, [queriesInput, states]);
217110

218111
return Tanstack.useQueries(
219112
{

0 commit comments

Comments
 (0)