From 2749ece6eb7faae885c0aadfb1202516c5a56b85 Mon Sep 17 00:00:00 2001 From: Ahmet Hos Date: Sat, 15 Nov 2025 18:24:23 +0100 Subject: [PATCH 1/3] fix(streamedQuery): fall back to initialValue when stream yields no values Add failing test for empty streams and update implementation to avoid returning undefined. Includes changeset for patch release. --- .changeset/puny-walls-eat.md | 5 +++ .../src/__tests__/streamedQuery.test.tsx | 31 +++++++++++++++++++ packages/query-core/src/streamedQuery.ts | 2 +- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .changeset/puny-walls-eat.md diff --git a/.changeset/puny-walls-eat.md b/.changeset/puny-walls-eat.md new file mode 100644 index 0000000000..39691503b3 --- /dev/null +++ b/.changeset/puny-walls-eat.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': patch +--- + +Fix streamedQuery to avoid returning undefined when the stream yields no values diff --git a/packages/query-core/src/__tests__/streamedQuery.test.tsx b/packages/query-core/src/__tests__/streamedQuery.test.tsx index 31a937f2a8..48860367ff 100644 --- a/packages/query-core/src/__tests__/streamedQuery.test.tsx +++ b/packages/query-core/src/__tests__/streamedQuery.test.tsx @@ -128,6 +128,37 @@ describe('streamedQuery', () => { unsubscribe() }) + test('should handle empty streams', async () => { + + const key = queryKey() + + const observer = new QueryObserver(queryClient, { + queryKey: key, + queryFn: streamedQuery({ + streamFn: () => { return {async *[Symbol.asyncIterator]() {}}}, + }), + }) + + const unsubscribe = observer.subscribe(vi.fn()) + + expect(observer.getCurrentResult()).toMatchObject({ + status: 'pending', + fetchStatus: 'fetching', + data: undefined, + }) + + await vi.advanceTimersByTimeAsync(50) + + expect(observer.getCurrentResult()).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: [], + }) + + unsubscribe() + + }) + test('should replace on refetch', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { diff --git a/packages/query-core/src/streamedQuery.ts b/packages/query-core/src/streamedQuery.ts index f4a60ee091..e9cb6ae99b 100644 --- a/packages/query-core/src/streamedQuery.ts +++ b/packages/query-core/src/streamedQuery.ts @@ -94,6 +94,6 @@ export function streamedQuery< context.client.setQueryData(context.queryKey, result) } - return context.client.getQueryData(context.queryKey)! + return context.client.getQueryData(context.queryKey) ?? initialValue } } From 709fad23e1bee71a916126713d904edc7284b184 Mon Sep 17 00:00:00 2001 From: Ahmet Hos Date: Sat, 15 Nov 2025 20:20:44 +0100 Subject: [PATCH 2/3] chore: address review nitpicks (JSDoc + test cleanup) --- packages/query-core/src/__tests__/streamedQuery.test.tsx | 4 +--- packages/query-core/src/streamedQuery.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/query-core/src/__tests__/streamedQuery.test.tsx b/packages/query-core/src/__tests__/streamedQuery.test.tsx index 48860367ff..084ea7d9db 100644 --- a/packages/query-core/src/__tests__/streamedQuery.test.tsx +++ b/packages/query-core/src/__tests__/streamedQuery.test.tsx @@ -129,13 +129,12 @@ describe('streamedQuery', () => { }) test('should handle empty streams', async () => { - const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: streamedQuery({ - streamFn: () => { return {async *[Symbol.asyncIterator]() {}}}, + streamFn: async function* () {}, }), }) @@ -156,7 +155,6 @@ describe('streamedQuery', () => { }) unsubscribe() - }) test('should replace on refetch', async () => { diff --git a/packages/query-core/src/streamedQuery.ts b/packages/query-core/src/streamedQuery.ts index e9cb6ae99b..2eb944d699 100644 --- a/packages/query-core/src/streamedQuery.ts +++ b/packages/query-core/src/streamedQuery.ts @@ -41,7 +41,7 @@ type StreamedQueryParams = * Set to `'replace'` to write all data to the cache once the stream ends. * @param reducer - A function to reduce the streamed chunks into the final data. * Defaults to a function that appends chunks to the end of the array. - * @param initialValue - Initial value to be used while the first chunk is being fetched. + * @param initialValue - Initial value to be used while the first chunk is being fetched, and returned if the stream yields no values. */ export function streamedQuery< TQueryFnData = unknown, From 1e269ac3d9d357f2a3a382a7618e915ccfcb54d1 Mon Sep 17 00:00:00 2001 From: Ahmet Hos Date: Sun, 16 Nov 2025 19:08:29 +0100 Subject: [PATCH 3/3] docs: note that initialValue is returned when the stream yields no values --- docs/reference/streamedQuery.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/streamedQuery.md b/docs/reference/streamedQuery.md index 8c70475ba3..fe5ed9f518 100644 --- a/docs/reference/streamedQuery.md +++ b/docs/reference/streamedQuery.md @@ -40,6 +40,6 @@ const query = queryOptions({ - If `TData` is not an array, you must provide a custom `reducer`. - `initialValue?: TData = TQueryFnData` - Optional - - Defines the initial data to be used while the first chunk is being fetched. + - Defines the initial data to be used while the first chunk is being fetched, and it is also returned when the stream yields no values. - It is mandatory when custom `reducer` is provided. - Defaults to an empty array.