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/beige-jars-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/react': patch
---

Fixed regression in useSuspendingQuery where `releaseHold is not a function` could be thrown during rendering.
16 changes: 6 additions & 10 deletions packages/react/src/hooks/suspense/suspense-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>) => {
const releaseTemporaryHold = React.useRef<(() => void) | undefined>(undefined);
const releaseTemporaryHold = React.useRef<() => void | undefined>(undefined);
const addedHoldTo = React.useRef<WatchedQuery<unknown> | 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) => {}
});
Expand Down Expand Up @@ -60,10 +60,6 @@ export const useTemporaryHold = (watchedQuery?: WatchedQuery<unknown>) => {
// Set a timeout to conditionally remove the temporary hold
setTimeout(checkHold, timeoutPollMs);
}

return {
releaseHold: releaseTemporaryHold.current
};
};

/**
Expand Down
6 changes: 1 addition & 5 deletions packages/react/src/hooks/suspense/useSingleSuspenseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const useSingleSuspenseQuery = <T = any>(

// Only use a temporary watched query if we don't have data yet.
const watchedQuery = data ? null : (store.getQuery(key, parsedQuery, options) as WatchedQuery<T[]>);
const { releaseHold } = useTemporaryHold(watchedQuery);
useTemporaryHold(watchedQuery);
React.useEffect(() => {
// Set the initial yielded data
// it should be available once we commit the component
Expand All @@ -47,10 +47,6 @@ export const useSingleSuspenseQuery = <T = any>(
setData(watchedQuery.state.data);
setError(null);
}

if (!watchedQuery?.state.isLoading) {
releaseHold();
}
}, []);

if (error != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}, []);

Expand Down
33 changes: 33 additions & 0 deletions packages/react/tests/useSuspenseQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => (
<React.StrictMode>
<PowerSyncContext.Provider value={db}>{children}</PowerSyncContext.Provider>
</React.StrictMode>
);
const { result } = renderHook(() => useWatchedQuerySuspenseSubscription(listsQuery), {
wrapper
});

// Initially, the query should be loading/suspended
expect(result.current).toBeDefined();
expect(result.current.data.length).toEqual(0);
});
});