Skip to content

Commit 3f9df96

Browse files
authored
fix: Tanstack error handling improvements (#420)
1 parent 36af0c8 commit 3f9df96

File tree

7 files changed

+195
-15
lines changed

7 files changed

+195
-15
lines changed

.changeset/brown-moose-chew.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/tanstack-react-query': patch
3+
---
4+
5+
Fixed issue with compilable queries needing a parameter value specified and fixed issue related to compilable query errors causing infinite rendering.

packages/tanstack-react-query/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"build": "tsc -b",
1616
"build:prod": "tsc -b --sourceMap false",
1717
"clean": "rm -rf lib tsconfig.tsbuildinfo",
18+
"test": "vitest",
1819
"watch": "tsc -b -w"
1920
},
2021
"repository": {
@@ -37,6 +38,7 @@
3738
"@tanstack/react-query": "^5.55.4"
3839
},
3940
"devDependencies": {
41+
"@testing-library/react": "^15.0.2",
4042
"@types/react": "^18.2.34",
4143
"jsdom": "^24.0.0",
4244
"react": "18.2.0",

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

+10-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parseQuery, type CompilableQuery, type ParsedQuery, type SQLWatchOptions } from '@powersync/common';
1+
import { parseQuery, type CompilableQuery } from '@powersync/common';
22
import { usePowerSync } from '@powersync/react';
33
import React from 'react';
44

@@ -65,15 +65,10 @@ function useQueryCore<
6565
throw new Error('PowerSync is not available');
6666
}
6767

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

72-
React.useEffect(() => {
73-
if (error) {
74-
setError(null);
75-
}
76-
}, [powerSync, query, parameters, options.queryKey]);
70+
const [tables, setTables] = React.useState<string[]>([]);
71+
const { query, parameters = [], ...resolvedOptions } = options;
7772

7873
let sqlStatement = '';
7974
let queryParameters = [];
@@ -85,7 +80,7 @@ function useQueryCore<
8580
sqlStatement = parsedQuery.sqlStatement;
8681
queryParameters = parsedQuery.parameters;
8782
} catch (e) {
88-
setError(e);
83+
error = e;
8984
}
9085
}
9186

@@ -97,12 +92,12 @@ function useQueryCore<
9792
const tables = await powerSync.resolveTables(sqlStatement, queryParameters);
9893
setTables(tables);
9994
} catch (e) {
100-
setError(e);
95+
error = e;
10196
}
10297
};
10398

10499
React.useEffect(() => {
105-
if (!query) return () => {};
100+
if (error || !query) return () => {};
106101

107102
(async () => {
108103
await fetchTables();
@@ -128,7 +123,7 @@ function useQueryCore<
128123
} catch (e) {
129124
return Promise.reject(e);
130125
}
131-
}, [powerSync, query, parameters, stringifiedKey, error]);
126+
}, [powerSync, query, parameters, stringifiedKey]);
132127

133128
React.useEffect(() => {
134129
if (error || !query) return () => {};
@@ -142,7 +137,7 @@ function useQueryCore<
142137
});
143138
},
144139
onError: (e) => {
145-
setError(e);
140+
error = e;
146141
}
147142
},
148143
{
@@ -151,7 +146,7 @@ function useQueryCore<
151146
}
152147
);
153148
return () => abort.abort();
154-
}, [powerSync, queryClient, stringifiedKey, tables, error]);
149+
}, [powerSync, queryClient, stringifiedKey, tables]);
155150

156151
return useQueryFn(
157152
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"compilerOptions": {
3+
"baseUrl": "./",
4+
"esModuleInterop": true,
5+
"jsx": "react",
6+
"rootDir": "../",
7+
"composite": true,
8+
"outDir": "./lib",
9+
"lib": ["esnext", "DOM"],
10+
"module": "esnext",
11+
"sourceMap": true,
12+
"moduleResolution": "node",
13+
"noFallthroughCasesInSwitch": true,
14+
"noImplicitReturns": true,
15+
"noImplicitUseStrict": false,
16+
"noStrictGenericChecks": false,
17+
"resolveJsonModule": true,
18+
"skipLibCheck": true,
19+
"target": "esnext"
20+
},
21+
"include": ["../src/**/*"]
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import * as commonSdk from '@powersync/common';
2+
import { cleanup, renderHook, waitFor } from '@testing-library/react';
3+
import React from 'react';
4+
import { beforeEach, describe, expect, it, vi } from 'vitest';
5+
import { PowerSyncContext } from '@powersync/react/';
6+
import { useQuery } from '../src/hooks/useQuery';
7+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
8+
9+
const mockPowerSync = {
10+
currentStatus: { status: 'initial' },
11+
registerListener: vi.fn(() => { }),
12+
resolveTables: vi.fn(() => ['table1', 'table2']),
13+
onChangeWithCallback: vi.fn(),
14+
getAll: vi.fn(() => Promise.resolve(['list1', 'list2']))
15+
};
16+
17+
vi.mock('./PowerSyncContext', () => ({
18+
useContext: vi.fn(() => mockPowerSync)
19+
}));
20+
21+
describe('useQuery', () => {
22+
let queryClient = new QueryClient({
23+
defaultOptions: {
24+
queries: {
25+
retry: false,
26+
},
27+
}
28+
})
29+
30+
const wrapper = ({ children }) => (
31+
<QueryClientProvider client={queryClient}>
32+
<PowerSyncContext.Provider value={mockPowerSync as any}>{children}</PowerSyncContext.Provider>
33+
</QueryClientProvider>
34+
);
35+
36+
beforeEach(() => {
37+
queryClient.clear();
38+
39+
vi.clearAllMocks();
40+
cleanup(); // Cleanup the DOM after each test
41+
});
42+
43+
44+
it('should set loading states on initial load', async () => {
45+
const { result } = renderHook(() => useQuery({
46+
queryKey: ['lists'],
47+
query: 'SELECT * from lists'
48+
}), { wrapper });
49+
const currentResult = result.current;
50+
expect(currentResult.isLoading).toEqual(true);
51+
expect(currentResult.isFetching).toEqual(true);
52+
});
53+
54+
it('should execute string queries', async () => {
55+
const query = () =>
56+
useQuery({
57+
queryKey: ['lists'],
58+
query: "SELECT * from lists"
59+
});
60+
const { result } = renderHook(query, { wrapper });
61+
62+
await vi.waitFor(() => {
63+
expect(result.current.data![0]).toEqual('list1');
64+
expect(result.current.data![1]).toEqual('list2');
65+
}, { timeout: 500 });
66+
});
67+
68+
it('should set error during query execution', async () => {
69+
const mockPowerSyncError = {
70+
currentStatus: { status: 'initial' },
71+
registerListener: vi.fn(() => { }),
72+
onChangeWithCallback: vi.fn(),
73+
resolveTables: vi.fn(() => ['table1', 'table2']),
74+
getAll: vi.fn(() => {
75+
throw new Error('some error');
76+
})
77+
};
78+
79+
const wrapper = ({ children }) => (
80+
<QueryClientProvider client={queryClient}>
81+
<PowerSyncContext.Provider value={mockPowerSyncError as any}>{children}</PowerSyncContext.Provider>
82+
</QueryClientProvider>
83+
);
84+
85+
const { result } = renderHook(() => useQuery({
86+
queryKey: ['lists'],
87+
query: 'SELECT * from lists'
88+
}), { wrapper });
89+
90+
await waitFor(
91+
async () => {
92+
expect(result.current.error).toEqual(Error('some error'));
93+
},
94+
{ timeout: 100 }
95+
);
96+
});
97+
98+
it('should execute compatible queries', async () => {
99+
const compilableQuery = {
100+
execute: () => [{ test: 'custom' }] as any,
101+
compile: () => ({ sql: 'SELECT * from lists' })
102+
} as commonSdk.CompilableQuery<any>;
103+
104+
const query = () =>
105+
useQuery({
106+
queryKey: ['lists'],
107+
query: compilableQuery
108+
});
109+
const { result } = renderHook(query, { wrapper });
110+
111+
await vi.waitFor(() => {
112+
expect(result.current.data![0].test).toEqual('custom');
113+
}, { timeout: 500 });
114+
});
115+
116+
it('should show an error if parsing the query results in an error', async () => {
117+
const compilableQuery = {
118+
execute: () => [] as any,
119+
compile: () => ({ sql: 'SELECT * from lists', parameters: ['param'] })
120+
} as commonSdk.CompilableQuery<any>;
121+
122+
const { result } = renderHook(
123+
() =>
124+
useQuery({
125+
queryKey: ['lists'],
126+
query: compilableQuery,
127+
parameters: ['redundant param']
128+
}),
129+
{ wrapper }
130+
);
131+
132+
await waitFor(
133+
async () => {
134+
const currentResult = result.current;
135+
expect(currentResult.isLoading).toEqual(false);
136+
expect(currentResult.isFetching).toEqual(false);
137+
expect(currentResult.error).toEqual(Error('You cannot pass parameters to a compiled query.'));
138+
expect(currentResult.data).toBeUndefined()
139+
},
140+
{ timeout: 100 }
141+
);
142+
});
143+
144+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineConfig, UserConfigExport } from 'vitest/config';
2+
3+
const config: UserConfigExport = {
4+
test: {
5+
environment: 'jsdom'
6+
}
7+
};
8+
9+
export default defineConfig(config);

pnpm-lock.yaml

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)