diff --git a/.changeset/brown-moose-chew.md b/.changeset/brown-moose-chew.md new file mode 100644 index 000000000..ae544869a --- /dev/null +++ b/.changeset/brown-moose-chew.md @@ -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. diff --git a/packages/tanstack-react-query/package.json b/packages/tanstack-react-query/package.json index 45a618c9d..4778b4fb6 100644 --- a/packages/tanstack-react-query/package.json +++ b/packages/tanstack-react-query/package.json @@ -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": { @@ -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", diff --git a/packages/tanstack-react-query/src/hooks/useQuery.ts b/packages/tanstack-react-query/src/hooks/useQuery.ts index 8c24bc569..aa1034ed4 100644 --- a/packages/tanstack-react-query/src/hooks/useQuery.ts +++ b/packages/tanstack-react-query/src/hooks/useQuery.ts @@ -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'; @@ -65,15 +65,10 @@ function useQueryCore< throw new Error('PowerSync is not available'); } - const [error, setError] = React.useState(null); - const [tables, setTables] = React.useState([]); - 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([]); + const { query, parameters = [], ...resolvedOptions } = options; let sqlStatement = ''; let queryParameters = []; @@ -85,7 +80,7 @@ function useQueryCore< sqlStatement = parsedQuery.sqlStatement; queryParameters = parsedQuery.parameters; } catch (e) { - setError(e); + error = e; } } @@ -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(); @@ -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 () => {}; @@ -142,7 +137,7 @@ function useQueryCore< }); }, onError: (e) => { - setError(e); + error = e; } }, { @@ -151,7 +146,7 @@ function useQueryCore< } ); return () => abort.abort(); - }, [powerSync, queryClient, stringifiedKey, tables, error]); + }, [powerSync, queryClient, stringifiedKey, tables]); return useQueryFn( { diff --git a/packages/tanstack-react-query/tests/tsconfig.json b/packages/tanstack-react-query/tests/tsconfig.json new file mode 100644 index 000000000..41af59e85 --- /dev/null +++ b/packages/tanstack-react-query/tests/tsconfig.json @@ -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/**/*"] +} diff --git a/packages/tanstack-react-query/tests/useQuery.test.tsx b/packages/tanstack-react-query/tests/useQuery.test.tsx new file mode 100644 index 000000000..e6aa2fd60 --- /dev/null +++ b/packages/tanstack-react-query/tests/useQuery.test.tsx @@ -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 }) => ( + + {children} + + ); + + 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 }) => ( + + {children} + + ); + + 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; + + 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; + + 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 } + ); + }); + +}); diff --git a/packages/tanstack-react-query/vitest.config.ts b/packages/tanstack-react-query/vitest.config.ts new file mode 100644 index 000000000..f96ac5630 --- /dev/null +++ b/packages/tanstack-react-query/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, UserConfigExport } from 'vitest/config'; + +const config: UserConfigExport = { + test: { + environment: 'jsdom' + } +}; + +export default defineConfig(config); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a973c8544..b0c7ef8c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1843,6 +1843,9 @@ importers: specifier: ^5.55.4 version: 5.59.14(react@18.2.0) devDependencies: + '@testing-library/react': + specifier: ^15.0.2 + version: 15.0.7(@types/react@18.3.11)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/react': specifier: ^18.2.34 version: 18.3.11