Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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/tall-dodos-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/tanstack-react-query': patch
---

Added Tanstack useQueries support. See https://tanstack.com/query/latest/docs/framework/react/reference/useQueries for more information.
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