diff --git a/.changeset/empty-donuts-repair.md b/.changeset/empty-donuts-repair.md new file mode 100644 index 000000000..fc8963daf --- /dev/null +++ b/.changeset/empty-donuts-repair.md @@ -0,0 +1,5 @@ +--- +'@powersync/react': patch +--- + +Fix "order and size of this array must remain constant" warning. diff --git a/packages/react/src/hooks/watched/watch-utils.ts b/packages/react/src/hooks/watched/watch-utils.ts index 82506433b..f079e976b 100644 --- a/packages/react/src/hooks/watched/watch-utils.ts +++ b/packages/react/src/hooks/watched/watch-utils.ts @@ -10,7 +10,11 @@ export type InternalHookOptions = { active: boolean; }; -export const checkQueryChanged = (query: WatchCompatibleQuery, options: AdditionalOptions) => { +interface WatchCompatibleQueryWithParams extends WatchCompatibleQuery { + stringifiedParameters?: string; +} + +export const checkQueryChanged = (query: WatchCompatibleQueryWithParams, options: AdditionalOptions) => { let _compiled: CompiledQuery; try { _compiled = query.compile(); @@ -19,7 +23,7 @@ export const checkQueryChanged = (query: WatchCompatibleQuery, options: Ad } const compiled = _compiled!; - const stringifiedParams = JSON.stringify(compiled.parameters); + const stringifiedParams = query.stringifiedParameters ?? JSON.stringify(compiled.parameters); const stringifiedOptions = JSON.stringify(options); const previousQueryRef = React.useRef({ sqlStatement: compiled.sql, stringifiedParams, stringifiedOptions }); @@ -45,15 +49,18 @@ export const constructCompatibleQuery = ( options: AdditionalOptions ) => { const powerSync = usePowerSync(); + const stringifiedParameters = React.useMemo(() => JSON.stringify(parameters), [parameters]); - const parsedQuery = React.useMemo>(() => { + const parsedQuery = React.useMemo>(() => { if (typeof query == 'string') { return { compile: () => ({ sql: query, parameters }), - execute: () => powerSync.getAll(query, parameters) + execute: () => powerSync.getAll(query, parameters), + // Setting this is a small optimization that avoids checkQueryChanged recomputing the JSON representation. + stringifiedParameters }; } else { return { @@ -66,9 +73,11 @@ export const constructCompatibleQuery = ( }; }, execute: () => query.execute() + // Note that we can't set stringifiedParameters here because we only know parameters after the query has been + // compiled. }; } - }, [query, powerSync, ...parameters]); + }, [query, powerSync, stringifiedParameters]); const queryChanged = checkQueryChanged(parsedQuery, options); diff --git a/packages/react/tests/useQuery.test.tsx b/packages/react/tests/useQuery.test.tsx index dfc9ef810..2ced417df 100644 --- a/packages/react/tests/useQuery.test.tsx +++ b/packages/react/tests/useQuery.test.tsx @@ -596,6 +596,49 @@ describe('useQuery', () => { ); }); + it('sohuld allow changing parameter array size', async () => { + const db = openPowerSync(); + + let currentQuery = { sql: 'SELECT ? AS a', params: ['foo'] }; + let listeners: (() => void)[] = []; + + const query = () => { + const current = React.useSyncExternalStore( + (onChange) => { + listeners.push(onChange); + return () => listeners.splice(listeners.indexOf(onChange), 1); + }, + () => currentQuery + ); + + return useQuery(current.sql, current.params); + }; + + const { result } = renderHook(query, { wrapper: ({ children }) => testWrapper({ children, db }) }); + + await vi.waitFor( + () => { + expect(result.current.data).toStrictEqual([{ a: 'foo' }]); + }, + { timeout: 500, interval: 50 } + ); + + // Now update the parameter + act(() => { + currentQuery = { sql: 'SELECT ? AS a, ? AS b', params: ['foo', 'bar'] }; + for (const listener of listeners) { + listener(); + } + }); + + await vi.waitFor( + () => { + expect(result.current.data).toStrictEqual([{ a: 'foo', b: 'bar' }]); + }, + { timeout: 500, interval: 50 } + ); + }); + it('should show an error if parsing the query results in an error', async () => { const db = openPowerSync();