Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/brown-moose-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/tanstack-react-query': patch
---

Fixed issue with compilable queries needing a parameter value specified and fixed issue related to compilable query errors causing infinite rendering.
2 changes: 2 additions & 0 deletions packages/tanstack-react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"build": "tsc -b",
"build:prod": "tsc -b --sourceMap false",
"clean": "rm -rf lib tsconfig.tsbuildinfo",
"test": "vitest",
"watch": "tsc -b -w"
},
"repository": {
Expand All @@ -37,6 +38,7 @@
"@tanstack/react-query": "^5.55.4"
},
"devDependencies": {
"@testing-library/react": "^15.0.2",
"@types/react": "^18.2.34",
"jsdom": "^24.0.0",
"react": "18.2.0",
Expand Down
25 changes: 10 additions & 15 deletions packages/tanstack-react-query/src/hooks/useQuery.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseQuery, type CompilableQuery, type ParsedQuery, type SQLWatchOptions } from '@powersync/common';
import { parseQuery, type CompilableQuery } from '@powersync/common';
import { usePowerSync } from '@powersync/react';
import React from 'react';

Expand Down Expand Up @@ -65,15 +65,10 @@ function useQueryCore<
throw new Error('PowerSync is not available');
}

const [error, setError] = React.useState<Error | null>(null);
const [tables, setTables] = React.useState<string[]>([]);
const { query, parameters, ...resolvedOptions } = options;
let error: Error | undefined = undefined;

React.useEffect(() => {
if (error) {
setError(null);
}
}, [powerSync, query, parameters, options.queryKey]);
const [tables, setTables] = React.useState<string[]>([]);
const { query, parameters = [], ...resolvedOptions } = options;

let sqlStatement = '';
let queryParameters = [];
Expand All @@ -85,7 +80,7 @@ function useQueryCore<
sqlStatement = parsedQuery.sqlStatement;
queryParameters = parsedQuery.parameters;
} catch (e) {
setError(e);
error = e;
}
}

Expand All @@ -97,12 +92,12 @@ function useQueryCore<
const tables = await powerSync.resolveTables(sqlStatement, queryParameters);
setTables(tables);
} catch (e) {
setError(e);
error = e;
}
};

React.useEffect(() => {
if (!query) return () => {};
if (error || !query) return () => {};

(async () => {
await fetchTables();
Expand All @@ -128,7 +123,7 @@ function useQueryCore<
} catch (e) {
return Promise.reject(e);
}
}, [powerSync, query, parameters, stringifiedKey, error]);
}, [powerSync, query, parameters, stringifiedKey]);

React.useEffect(() => {
if (error || !query) return () => {};
Expand All @@ -142,7 +137,7 @@ function useQueryCore<
});
},
onError: (e) => {
setError(e);
error = e;
}
},
{
Expand All @@ -151,7 +146,7 @@ function useQueryCore<
}
);
return () => abort.abort();
}, [powerSync, queryClient, stringifiedKey, tables, error]);
}, [powerSync, queryClient, stringifiedKey, tables]);

return useQueryFn(
{
Expand Down
22 changes: 22 additions & 0 deletions packages/tanstack-react-query/tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"baseUrl": "./",
"esModuleInterop": true,
"jsx": "react",
"rootDir": "../",
"composite": true,
"outDir": "./lib",
"lib": ["esnext", "DOM"],
"module": "esnext",
"sourceMap": true,
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noImplicitUseStrict": false,
"noStrictGenericChecks": false,
"resolveJsonModule": true,
"skipLibCheck": true,
"target": "esnext"
},
"include": ["../src/**/*"]
}
144 changes: 144 additions & 0 deletions packages/tanstack-react-query/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import * as commonSdk from '@powersync/common';
import { cleanup, renderHook, waitFor } from '@testing-library/react';
import React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { PowerSyncContext } from '@powersync/react/';
import { useQuery } from '../src/hooks/useQuery';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const mockPowerSync = {
currentStatus: { status: 'initial' },
registerListener: vi.fn(() => { }),
resolveTables: vi.fn(() => ['table1', 'table2']),
onChangeWithCallback: vi.fn(),
getAll: vi.fn(() => Promise.resolve(['list1', 'list2']))
};

vi.mock('./PowerSyncContext', () => ({
useContext: vi.fn(() => mockPowerSync)
}));

describe('useQuery', () => {
let queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
}
})

const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
<PowerSyncContext.Provider value={mockPowerSync as any}>{children}</PowerSyncContext.Provider>
</QueryClientProvider>
);

beforeEach(() => {
queryClient.clear();

vi.clearAllMocks();
cleanup(); // Cleanup the DOM after each test
});


it('should set loading states on initial load', async () => {
const { result } = renderHook(() => useQuery({
queryKey: ['lists'],
query: 'SELECT * from lists'
}), { wrapper });
const currentResult = result.current;
expect(currentResult.isLoading).toEqual(true);
expect(currentResult.isFetching).toEqual(true);
});

it('should execute string queries', async () => {
const query = () =>
useQuery({
queryKey: ['lists'],
query: "SELECT * from lists"
});
const { result } = renderHook(query, { wrapper });

await vi.waitFor(() => {
expect(result.current.data![0]).toEqual('list1');
expect(result.current.data![1]).toEqual('list2');
}, { timeout: 500 });
});

it('should set error during query execution', async () => {
const mockPowerSyncError = {
currentStatus: { status: 'initial' },
registerListener: vi.fn(() => { }),
onChangeWithCallback: vi.fn(),
resolveTables: vi.fn(() => ['table1', 'table2']),
getAll: vi.fn(() => {
throw new Error('some error');
})
};

const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
<PowerSyncContext.Provider value={mockPowerSyncError as any}>{children}</PowerSyncContext.Provider>
</QueryClientProvider>
);

const { result } = renderHook(() => useQuery({
queryKey: ['lists'],
query: 'SELECT * from lists'
}), { wrapper });

await waitFor(
async () => {
expect(result.current.error).toEqual(Error('some error'));
},
{ timeout: 100 }
);
});

it('should execute compatible queries', async () => {
const compilableQuery = {
execute: () => [{ test: 'custom' }] as any,
compile: () => ({ sql: 'SELECT * from lists' })
} as commonSdk.CompilableQuery<any>;

const query = () =>
useQuery({
queryKey: ['lists'],
query: compilableQuery
});
const { result } = renderHook(query, { wrapper });

await vi.waitFor(() => {
expect(result.current.data![0].test).toEqual('custom');
}, { timeout: 500 });
});

it('should show an error if parsing the query results in an error', async () => {
const compilableQuery = {
execute: () => [] as any,
compile: () => ({ sql: 'SELECT * from lists', parameters: ['param'] })
} as commonSdk.CompilableQuery<any>;

const { result } = renderHook(
() =>
useQuery({
queryKey: ['lists'],
query: compilableQuery,
parameters: ['redundant param']
}),
{ wrapper }
);

await waitFor(
async () => {
const currentResult = result.current;
expect(currentResult.isLoading).toEqual(false);
expect(currentResult.isFetching).toEqual(false);
expect(currentResult.error).toEqual(Error('You cannot pass parameters to a compiled query.'));
expect(currentResult.data).toBeUndefined()
},
{ timeout: 100 }
);
});

});
9 changes: 9 additions & 0 deletions packages/tanstack-react-query/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig, UserConfigExport } from 'vitest/config';

const config: UserConfigExport = {
test: {
environment: 'jsdom'
}
};

export default defineConfig(config);
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.