diff --git a/.changeset/beige-jars-whisper.md b/.changeset/beige-jars-whisper.md new file mode 100644 index 000000000..6154e5cce --- /dev/null +++ b/.changeset/beige-jars-whisper.md @@ -0,0 +1,5 @@ +--- +'@powersync/react': patch +--- + +Fixed regression in useSuspendingQuery where `releaseHold is not a function` could be thrown during rendering. diff --git a/packages/react/src/hooks/suspense/suspense-utils.ts b/packages/react/src/hooks/suspense/suspense-utils.ts index f80621587..b371ec0c8 100644 --- a/packages/react/src/hooks/suspense/suspense-utils.ts +++ b/packages/react/src/hooks/suspense/suspense-utils.ts @@ -7,23 +7,23 @@ import React from 'react'; * before this component is committed. The promise will release it's listener once the query is no longer loading. * This temporary hold is used to ensure that the query is not disposed in the interim. * Creates a subscription for state change which creates a temporary hold on the query - * @returns a function to release the hold */ export const useTemporaryHold = (watchedQuery?: WatchedQuery) => { - const releaseTemporaryHold = React.useRef<(() => void) | undefined>(undefined); + const releaseTemporaryHold = React.useRef<() => void | undefined>(undefined); const addedHoldTo = React.useRef | undefined>(undefined); if (addedHoldTo.current !== watchedQuery) { + // The query changed, we no longer need the previous hold if present releaseTemporaryHold.current?.(); + releaseTemporaryHold.current = undefined; addedHoldTo.current = watchedQuery; if (!watchedQuery || !watchedQuery.state.isLoading) { - // No query to hold or no reason to hold, return a no-op - return { - releaseHold: () => {} - }; + // No query to hold or no reason to hold, return + return; } + // Create a hold by subscribing const disposeSubscription = watchedQuery.registerListener({ onStateChange: (state) => {} }); @@ -60,10 +60,6 @@ export const useTemporaryHold = (watchedQuery?: WatchedQuery) => { // Set a timeout to conditionally remove the temporary hold setTimeout(checkHold, timeoutPollMs); } - - return { - releaseHold: releaseTemporaryHold.current - }; }; /** diff --git a/packages/react/src/hooks/suspense/useSingleSuspenseQuery.ts b/packages/react/src/hooks/suspense/useSingleSuspenseQuery.ts index f778e40e4..090c82423 100644 --- a/packages/react/src/hooks/suspense/useSingleSuspenseQuery.ts +++ b/packages/react/src/hooks/suspense/useSingleSuspenseQuery.ts @@ -37,7 +37,7 @@ export const useSingleSuspenseQuery = ( // Only use a temporary watched query if we don't have data yet. const watchedQuery = data ? null : (store.getQuery(key, parsedQuery, options) as WatchedQuery); - const { releaseHold } = useTemporaryHold(watchedQuery); + useTemporaryHold(watchedQuery); React.useEffect(() => { // Set the initial yielded data // it should be available once we commit the component @@ -47,10 +47,6 @@ export const useSingleSuspenseQuery = ( setData(watchedQuery.state.data); setError(null); } - - if (!watchedQuery?.state.isLoading) { - releaseHold(); - } }, []); if (error != null) { diff --git a/packages/react/src/hooks/suspense/useWatchedQuerySuspenseSubscription.ts b/packages/react/src/hooks/suspense/useWatchedQuerySuspenseSubscription.ts index 887586f8c..8fcedf122 100644 --- a/packages/react/src/hooks/suspense/useWatchedQuerySuspenseSubscription.ts +++ b/packages/react/src/hooks/suspense/useWatchedQuerySuspenseSubscription.ts @@ -29,7 +29,7 @@ export const useWatchedQuerySuspenseSubscription = < >( query: Query ): Query['state'] => { - const { releaseHold } = useTemporaryHold(query); + useTemporaryHold(query); // Force update state function const [, setUpdateCounter] = React.useState(0); @@ -44,12 +44,6 @@ export const useWatchedQuerySuspenseSubscription = < } }); - // This runs on the first iteration before the component is suspended - // We should only release the hold once the component is no longer loading - if (!query.state.isLoading) { - releaseHold(); - } - return dispose; }, []); diff --git a/packages/react/tests/useSuspenseQuery.test.tsx b/packages/react/tests/useSuspenseQuery.test.tsx index ecc5e28a3..45c0b0269 100644 --- a/packages/react/tests/useSuspenseQuery.test.tsx +++ b/packages/react/tests/useSuspenseQuery.test.tsx @@ -299,4 +299,37 @@ describe('useSuspenseQuery', () => { expect(newResult.current).not.null; expect(newResult.current.data.length).toEqual(1); }); + + it('should use an existing loaded WatchedQuery instance', async () => { + const db = openPowerSync(); + + const listsQuery = db + .query({ + sql: `SELECT * FROM lists`, + parameters: [] + }) + .watch(); + + // Ensure the query has loaded before passing it to the hook. + // This means we don't require a temporary hold + await waitFor( + () => { + expect(listsQuery.state.isLoading).toBe(false); + }, + { timeout: 1000 } + ); + + const wrapper = ({ children }) => ( + + {children} + + ); + const { result } = renderHook(() => useWatchedQuerySuspenseSubscription(listsQuery), { + wrapper + }); + + // Initially, the query should be loading/suspended + expect(result.current).toBeDefined(); + expect(result.current.data.length).toEqual(0); + }); });