Skip to content

Commit dce523a

Browse files
React 19 Fixes (#688)
1 parent 7f2c53d commit dce523a

File tree

5 files changed

+46
-22
lines changed

5 files changed

+46
-22
lines changed

.changeset/beige-jars-whisper.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/react': patch
3+
---
4+
5+
Fixed regression in useSuspendingQuery where `releaseHold is not a function` could be thrown during rendering.

packages/react/src/hooks/suspense/suspense-utils.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,23 @@ import React from 'react';
77
* before this component is committed. The promise will release it's listener once the query is no longer loading.
88
* This temporary hold is used to ensure that the query is not disposed in the interim.
99
* Creates a subscription for state change which creates a temporary hold on the query
10-
* @returns a function to release the hold
1110
*/
1211
export const useTemporaryHold = (watchedQuery?: WatchedQuery<unknown>) => {
13-
const releaseTemporaryHold = React.useRef<(() => void) | undefined>(undefined);
12+
const releaseTemporaryHold = React.useRef<() => void | undefined>(undefined);
1413
const addedHoldTo = React.useRef<WatchedQuery<unknown> | undefined>(undefined);
1514

1615
if (addedHoldTo.current !== watchedQuery) {
16+
// The query changed, we no longer need the previous hold if present
1717
releaseTemporaryHold.current?.();
18+
releaseTemporaryHold.current = undefined;
1819
addedHoldTo.current = watchedQuery;
1920

2021
if (!watchedQuery || !watchedQuery.state.isLoading) {
21-
// No query to hold or no reason to hold, return a no-op
22-
return {
23-
releaseHold: () => {}
24-
};
22+
// No query to hold or no reason to hold, return
23+
return;
2524
}
2625

26+
// Create a hold by subscribing
2727
const disposeSubscription = watchedQuery.registerListener({
2828
onStateChange: (state) => {}
2929
});
@@ -60,10 +60,6 @@ export const useTemporaryHold = (watchedQuery?: WatchedQuery<unknown>) => {
6060
// Set a timeout to conditionally remove the temporary hold
6161
setTimeout(checkHold, timeoutPollMs);
6262
}
63-
64-
return {
65-
releaseHold: releaseTemporaryHold.current
66-
};
6763
};
6864

6965
/**

packages/react/src/hooks/suspense/useSingleSuspenseQuery.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const useSingleSuspenseQuery = <T = any>(
3737

3838
// Only use a temporary watched query if we don't have data yet.
3939
const watchedQuery = data ? null : (store.getQuery(key, parsedQuery, options) as WatchedQuery<T[]>);
40-
const { releaseHold } = useTemporaryHold(watchedQuery);
40+
useTemporaryHold(watchedQuery);
4141
React.useEffect(() => {
4242
// Set the initial yielded data
4343
// it should be available once we commit the component
@@ -47,10 +47,6 @@ export const useSingleSuspenseQuery = <T = any>(
4747
setData(watchedQuery.state.data);
4848
setError(null);
4949
}
50-
51-
if (!watchedQuery?.state.isLoading) {
52-
releaseHold();
53-
}
5450
}, []);
5551

5652
if (error != null) {

packages/react/src/hooks/suspense/useWatchedQuerySuspenseSubscription.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const useWatchedQuerySuspenseSubscription = <
2929
>(
3030
query: Query
3131
): Query['state'] => {
32-
const { releaseHold } = useTemporaryHold(query);
32+
useTemporaryHold(query);
3333

3434
// Force update state function
3535
const [, setUpdateCounter] = React.useState(0);
@@ -44,12 +44,6 @@ export const useWatchedQuerySuspenseSubscription = <
4444
}
4545
});
4646

47-
// This runs on the first iteration before the component is suspended
48-
// We should only release the hold once the component is no longer loading
49-
if (!query.state.isLoading) {
50-
releaseHold();
51-
}
52-
5347
return dispose;
5448
}, []);
5549

packages/react/tests/useSuspenseQuery.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,37 @@ describe('useSuspenseQuery', () => {
299299
expect(newResult.current).not.null;
300300
expect(newResult.current.data.length).toEqual(1);
301301
});
302+
303+
it('should use an existing loaded WatchedQuery instance', async () => {
304+
const db = openPowerSync();
305+
306+
const listsQuery = db
307+
.query({
308+
sql: `SELECT * FROM lists`,
309+
parameters: []
310+
})
311+
.watch();
312+
313+
// Ensure the query has loaded before passing it to the hook.
314+
// This means we don't require a temporary hold
315+
await waitFor(
316+
() => {
317+
expect(listsQuery.state.isLoading).toBe(false);
318+
},
319+
{ timeout: 1000 }
320+
);
321+
322+
const wrapper = ({ children }) => (
323+
<React.StrictMode>
324+
<PowerSyncContext.Provider value={db}>{children}</PowerSyncContext.Provider>
325+
</React.StrictMode>
326+
);
327+
const { result } = renderHook(() => useWatchedQuerySuspenseSubscription(listsQuery), {
328+
wrapper
329+
});
330+
331+
// Initially, the query should be loading/suspended
332+
expect(result.current).toBeDefined();
333+
expect(result.current.data.length).toEqual(0);
334+
});
302335
});

0 commit comments

Comments
 (0)