Skip to content

feat: Tanstack useQueries support #642

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
192 changes: 192 additions & 0 deletions packages/tanstack-react-query/src/hooks/usePowerSyncQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { type CompilableQuery, parseQuery } from '@powersync/common';
import { usePowerSync } from '@powersync/react';
import { useEffect, useState, useCallback, useMemo } from 'react';
import * as Tanstack from '@tanstack/react-query';

export type UsePowerSyncQueriesInput = {
query?: string | CompilableQuery<unknown>;
parameters?: unknown[];
queryKey: Tanstack.QueryKey;
}[];

export type UsePowerSyncQueriesOutput = {
sqlStatement: string;
queryParameters: unknown[];
tables: string[];
error?: Error;
queryFn: () => Promise<unknown[]>;
}[];

export function usePowerSyncQueries(
queries: UsePowerSyncQueriesInput,
queryClient: Tanstack.QueryClient
): UsePowerSyncQueriesOutput {
const powerSync = usePowerSync();

const [tablesArr, setTablesArr] = useState<string[][]>(() => queries.map(() => []));
const [errorsArr, setErrorsArr] = useState<(Error | undefined)[]>(() => queries.map(() => undefined));

const updateTablesArr = useCallback((tables: string[], idx: number) => {
setTablesArr((prev) => {
if (JSON.stringify(prev[idx]) === JSON.stringify(tables)) return prev;
const next = [...prev];
next[idx] = tables;
return next;
});
}, []);

const updateErrorsArr = useCallback((error: Error | undefined, idx: number) => {
setErrorsArr((prev) => {
if (prev[idx]?.message === error?.message) return prev;
const next = [...prev];
next[idx] = error;
return next;
});
}, []);

const parsedQueries = useMemo(
() =>
queries.map((queryInput) => {
const { query, parameters = [], queryKey } = queryInput;

if (!query) {
return {
query,
parameters,
queryKey,
sqlStatement: '',
queryParameters: [],
parseError: undefined
};
}

try {
const parsed = parseQuery(query, parameters);
return {
query,
parameters,
queryKey,
sqlStatement: parsed.sqlStatement,
queryParameters: parsed.parameters,
parseError: undefined
};
} catch (e) {
return {
query,
parameters,
queryKey,
sqlStatement: '',
queryParameters: [],
parseError: e as Error
};
}
}),
[queries]
);

useEffect(() => {
parsedQueries.forEach((pq, idx) => {
if (pq.parseError) {
updateErrorsArr(pq.parseError, idx);
}
});
}, [parsedQueries, updateErrorsArr]);

const stringifiedQueriesDeps = JSON.stringify(
parsedQueries.map((q) => ({
sql: q.sqlStatement,
params: q.queryParameters
}))
);

useEffect(() => {
const listeners = parsedQueries.map((pq, idx) => {
if (pq.parseError || !pq.query) {
return null;
}

(async () => {
try {
const tables = await powerSync.resolveTables(pq.sqlStatement, pq.queryParameters);
updateTablesArr(tables, idx);
} catch (e) {
updateErrorsArr(e as Error, idx);
}
})();

return powerSync.registerListener({
schemaChanged: async () => {
try {
const tables = await powerSync.resolveTables(pq.sqlStatement, pq.queryParameters);
updateTablesArr(tables, idx);
queryClient.invalidateQueries({ queryKey: pq.queryKey });
} catch (e) {
updateErrorsArr(e as Error, idx);
}
}
});
});

return () => {
listeners.forEach((l) => l?.());
};
}, [powerSync, queryClient, stringifiedQueriesDeps, updateTablesArr, updateErrorsArr]);

const stringifiedQueryKeys = JSON.stringify(parsedQueries.map((q) => q.queryKey));

useEffect(() => {
const aborts = parsedQueries.map((pq, idx) => {
if (pq.parseError || !pq.query) {
return null;
}

const abort = new AbortController();

powerSync.onChangeWithCallback(
{
onChange: () => {
queryClient.invalidateQueries({ queryKey: pq.queryKey });
},
onError: (e) => {
updateErrorsArr(e, idx);
}
},
{
tables: tablesArr[idx],
signal: abort.signal
}
);

return abort;
});

return () => aborts.forEach((a) => a?.abort());
}, [powerSync, queryClient, tablesArr, updateErrorsArr, stringifiedQueryKeys]);

return useMemo(() => {
return parsedQueries.map((pq, idx) => {
const error = errorsArr[idx] || pq.parseError;

const queryFn = async () => {
if (error) throw error;
if (!pq.query) throw new Error('No query provided');

try {
return typeof pq.query === 'string'
? await powerSync.getAll(pq.sqlStatement, pq.queryParameters)
: await pq.query.execute();
} catch (e) {
throw e;
}
};

return {
sqlStatement: pq.sqlStatement,
queryParameters: pq.queryParameters,
tables: tablesArr[idx],
error,
queryFn
};
});
}, [parsedQueries, errorsArr, tablesArr, powerSync]);
}
127 changes: 127 additions & 0 deletions packages/tanstack-react-query/src/hooks/useQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { type CompilableQuery } from '@powersync/common';
import { usePowerSync } from '@powersync/react';
import * as Tanstack from '@tanstack/react-query';
import { useMemo } from 'react';
import { usePowerSyncQueries } from './usePowerSyncQueries';

export type PowerSyncQueryOptions<T> = {
query?: string | CompilableQuery<T>;
parameters?: any[];
};

export type PowerSyncQueryOption<T = unknown[]> = Tanstack.UseQueryOptions<T[]> & PowerSyncQueryOptions<T>;

export type InferQueryResults<TQueries extends readonly unknown[]> = {
[K in keyof TQueries]: TQueries[K] extends { query: CompilableQuery<infer TData> }
? Tanstack.UseQueryResult<TData[]>
: Tanstack.UseQueryResult<unknown[]>;
};

export type ExplicitQueryResults<T extends readonly unknown[]> = {
[K in keyof T]: Tanstack.UseQueryResult<T[K][]>;
};

export type EnhancedInferQueryResults<TQueries extends readonly unknown[]> = {
[K in keyof TQueries]: TQueries[K] extends { query: CompilableQuery<infer TData> }
? Tanstack.UseQueryResult<TData[]> & { queryKey: Tanstack.QueryKey }
: Tanstack.UseQueryResult<unknown[]> & { queryKey: Tanstack.QueryKey };
};

export type EnhancedExplicitQueryResults<T extends readonly unknown[]> = {
[K in keyof T]: Tanstack.UseQueryResult<T[K][]> & { queryKey: Tanstack.QueryKey };
};

// Explicit generic typing with combine
export function useQueries<T extends readonly unknown[], TCombined>(
options: {
queries: readonly [...{ [K in keyof T]: PowerSyncQueryOption<T[K]> }];
combine: (results: EnhancedExplicitQueryResults<T>) => TCombined;
},
queryClient?: Tanstack.QueryClient
): TCombined;

// Explicit generic typing without combine
export function useQueries<T extends readonly unknown[]>(
options: {
queries: readonly [...{ [K in keyof T]: PowerSyncQueryOption<T[K]> }];
combine?: undefined;
},
queryClient?: Tanstack.QueryClient
): ExplicitQueryResults<T>;

// Auto inference with combine
export function useQueries<TQueries extends readonly PowerSyncQueryOption[], TCombined>(
options: {
queries: readonly [...TQueries];
combine: (results: EnhancedInferQueryResults<TQueries>) => TCombined;
},
queryClient?: Tanstack.QueryClient
): TCombined;

// Auto inference without combine
export function useQueries<TQueries extends readonly PowerSyncQueryOption[]>(
options: {
queries: readonly [...TQueries];
combine?: undefined;
},
queryClient?: Tanstack.QueryClient
): InferQueryResults<TQueries>;

// Implementation
export function useQueries(
options: {
queries: readonly (Tanstack.UseQueryOptions & PowerSyncQueryOptions<unknown>)[];
combine?: (results: (Tanstack.UseQueryResult<unknown, unknown> & { queryKey: Tanstack.QueryKey })[]) => unknown;
},
queryClient: Tanstack.QueryClient = Tanstack.useQueryClient()
) {
const powerSync = usePowerSync();

if (!powerSync) {
throw new Error('PowerSync is not available');
}

const queriesInput = options.queries;

const powerSyncQueriesInput = useMemo(
() =>
queriesInput.map((queryOptions) => ({
query: queryOptions.query,
parameters: queryOptions.parameters,
queryKey: queryOptions.queryKey
})),
[queriesInput]
);

const states = usePowerSyncQueries(powerSyncQueriesInput, queryClient);

const queries = useMemo(() => {
return queriesInput.map((queryOptions, idx) => {
const { query, parameters, ...rest } = queryOptions;
const state = states[idx];

return {
...rest,
queryFn: query ? state.queryFn : rest.queryFn,
queryKey: rest.queryKey
};
});
}, [queriesInput, states]);

return Tanstack.useQueries(
{
queries: queries as Tanstack.QueriesOptions<any>,
combine: options.combine
? (results) => {
const enhancedResultsWithQueryKey = results.map((result, index) => ({
...result,
queryKey: queries[index].queryKey
}));

return options.combine?.(enhancedResultsWithQueryKey);
}
: undefined
},
queryClient
);
}
Loading