Skip to content

Commit 4d67c1c

Browse files
authored
feat(core): streamedQuery (#8814)
* feat: streamedQuery * feat: streaming example * chore: move * chore: fix lock file * docs: streamedQuery
1 parent f03b109 commit 4d67c1c

19 files changed

+1297
-395
lines changed

docs/config.json

+8
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,10 @@
605605
"label": "QueriesObserver",
606606
"to": "reference/QueriesObserver"
607607
},
608+
{
609+
"label": "streamedQuery",
610+
"to": "reference/streamedQuery"
611+
},
608612
{
609613
"label": "focusManager",
610614
"to": "reference/focusManager"
@@ -957,6 +961,10 @@
957961
{
958962
"label": "Devtools Embedded Panel",
959963
"to": "framework/react/examples/devtools-panel"
964+
},
965+
{
966+
"label": "Chat example (streaming)",
967+
"to": "framework/react/examples/chat"
960968
}
961969
]
962970
},

docs/reference/QueryObserver.md

-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ id: QueryObserver
33
title: QueryObserver
44
---
55

6-
## `QueryObserver`
7-
86
The `QueryObserver` can be used to observe and switch between queries.
97

108
```tsx

docs/reference/streamedQuery.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
id: streamedQuery
3+
title: streamedQuery
4+
---
5+
6+
`streamedQuery` is a helper function to create a query function that streams data from an [AsyncIterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator). Data will be an Array of all the chunks received. The query will be in a `pending` state until the first chunk of data is received, but will go to `success` after that. The query will stay in fetchStatus `fetching` until the stream ends.
7+
8+
```tsx
9+
const query = queryOptions({
10+
queryKey: ['data'],
11+
queryFn: streamedQuery({
12+
queryFn: fetchDataInChunks,
13+
}),
14+
})
15+
```
16+
17+
**Options**
18+
19+
- `queryFn: (context: QueryFunctionContext) => Promise<AsyncIterable<TData>>`
20+
- **Required**
21+
- The function that returns a Promise of an AsyncIterable of data to stream in.
22+
- Receives a [QueryFunctionContext](../guides/query-functions.md#queryfunctioncontext)
23+
- `refetchMode?: 'append' | 'reset'`
24+
- optional
25+
- when set to `'reset'`, the query will erase all data and go back into `pending` state when a refetch occurs.
26+
- when set ot `'append'`, data will be appended on a refetch.
27+
- defaults to `'reset'`

examples/react/chat/.eslintrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["plugin:react/jsx-runtime", "plugin:react-hooks/recommended"]
3+
}

examples/react/chat/.gitignore

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
pnpm-lock.yaml
15+
yarn.lock
16+
package-lock.json
17+
18+
# misc
19+
.DS_Store
20+
.env.local
21+
.env.development.local
22+
.env.test.local
23+
.env.production.local
24+
25+
npm-debug.log*
26+
yarn-debug.log*
27+
yarn-error.log*

examples/react/chat/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install`
6+
- `npm run dev`

examples/react/chat/index.html

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<meta name="theme-color" content="#000000" />
8+
9+
<title>TanStack Query React Simple Example App</title>
10+
</head>
11+
<body>
12+
<noscript>You need to enable JavaScript to run this app.</noscript>
13+
<div id="root"></div>
14+
<script type="module" src="/src/index.tsx"></script>
15+
</body>
16+
</html>

examples/react/chat/package.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@tanstack/query-example-chat",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "vite build",
8+
"preview": "vite preview"
9+
},
10+
"dependencies": {
11+
"@tanstack/react-query": "^5.68.0",
12+
"@tanstack/react-query-devtools": "^5.68.0",
13+
"react": "^19.0.0",
14+
"react-dom": "^19.0.0"
15+
},
16+
"devDependencies": {
17+
"@tailwindcss/vite": "^4.0.14",
18+
"@vitejs/plugin-react": "^4.3.3",
19+
"tailwindcss": "^4.0.14",
20+
"typescript": "5.8.2",
21+
"vite": "^5.3.5"
22+
}
23+
}
Loading

examples/react/chat/src/chat.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {
2+
queryOptions,
3+
experimental_streamedQuery as streamedQuery,
4+
} from '@tanstack/react-query'
5+
6+
const answers = [
7+
"I'm just an example chat, I can't really answer any questions :(".split(' '),
8+
'TanStack is great. Would you like to know more?'.split(' '),
9+
]
10+
11+
function chatAnswer(_question: string) {
12+
return {
13+
async *[Symbol.asyncIterator]() {
14+
const answer = answers[Math.floor(Math.random() * answers.length)]
15+
let index = 0
16+
while (index < answer.length) {
17+
await new Promise((resolve) =>
18+
setTimeout(resolve, 100 + Math.random() * 300),
19+
)
20+
yield answer[index++]
21+
}
22+
},
23+
}
24+
}
25+
26+
export const chatQueryOptions = (question: string) =>
27+
queryOptions({
28+
queryKey: ['chat', question],
29+
queryFn: streamedQuery({
30+
queryFn: () => chatAnswer(question),
31+
}),
32+
staleTime: Infinity,
33+
})

examples/react/chat/src/index.tsx

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import ReactDOM from 'react-dom/client'
2+
import {
3+
QueryClient,
4+
QueryClientProvider,
5+
useQuery,
6+
} from '@tanstack/react-query'
7+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
8+
9+
import './style.css'
10+
import { useState } from 'react'
11+
import { chatQueryOptions } from './chat'
12+
import { Message } from './message'
13+
14+
const queryClient = new QueryClient()
15+
16+
export default function App() {
17+
return (
18+
<QueryClientProvider client={queryClient}>
19+
<ReactQueryDevtools />
20+
<Example />
21+
</QueryClientProvider>
22+
)
23+
}
24+
25+
function ChatMessage({ question }: { question: string }) {
26+
const { error, data = [], isFetching } = useQuery(chatQueryOptions(question))
27+
28+
if (error) return 'An error has occurred: ' + error.message
29+
30+
return (
31+
<div>
32+
<Message message={{ content: question, isQuestion: true }} />
33+
<Message
34+
inProgress={isFetching}
35+
message={{ content: data.join(' '), isQuestion: false }}
36+
/>
37+
</div>
38+
)
39+
}
40+
41+
function Example() {
42+
const [questions, setQuestions] = useState<Array<string>>([])
43+
const [currentQuestion, setCurrentQuestion] = useState('')
44+
45+
const submitMessage = () => {
46+
setQuestions([...questions, currentQuestion])
47+
setCurrentQuestion('')
48+
}
49+
50+
return (
51+
<div className="flex flex-col h-screen max-w-3xl mx-auto p-4">
52+
<h1 className="text-3xl font-bold text-gray-800">
53+
TanStack Chat Example
54+
</h1>
55+
<div className="overflow-y-auto mb-4 space-y-4">
56+
{questions.map((question) => (
57+
<ChatMessage key={question} question={question} />
58+
))}
59+
</div>
60+
61+
<div className="flex items-center space-x-2">
62+
<input
63+
className="flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-100"
64+
value={currentQuestion}
65+
onChange={(e) => setCurrentQuestion(e.target.value)}
66+
onKeyDown={(e) => {
67+
if (e.key === 'Enter') {
68+
submitMessage()
69+
}
70+
}}
71+
placeholder="Type your message..."
72+
/>
73+
<button
74+
onClick={submitMessage}
75+
disabled={!currentQuestion.trim()}
76+
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white font-semibold px-4 py-2 rounded-2xl shadow-md transition"
77+
>
78+
<span>Send</span>
79+
<svg
80+
xmlns="http://www.w3.org/2000/svg"
81+
width="24"
82+
height="24"
83+
viewBox="0 0 24 24"
84+
fill="none"
85+
stroke="currentColor"
86+
stroke-width="2"
87+
stroke-linecap="round"
88+
stroke-linejoin="round"
89+
>
90+
<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" />
91+
<path d="m21.854 2.147-10.94 10.939" />
92+
</svg>
93+
</button>
94+
</div>
95+
</div>
96+
)
97+
}
98+
99+
const rootElement = document.getElementById('root') as HTMLElement
100+
ReactDOM.createRoot(rootElement).render(<App />)

examples/react/chat/src/message.tsx

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export function Message({
2+
inProgress,
3+
message,
4+
}: {
5+
inProgress?: boolean
6+
message: { content: string; isQuestion: boolean }
7+
}) {
8+
return (
9+
<div
10+
className={`flex ${message.isQuestion ? 'justify-end' : 'justify-start'}`}
11+
>
12+
<div
13+
className={`max-w-[80%] rounded-lg p-3 ${
14+
message.isQuestion
15+
? 'bg-blue-500 text-white'
16+
: 'bg-gray-200 text-gray-800'
17+
}`}
18+
>
19+
{message.content}
20+
{inProgress ? '...' : null}
21+
</div>
22+
</div>
23+
)
24+
}

examples/react/chat/src/style.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import 'tailwindcss';

examples/react/chat/tsconfig.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"useDefineForClassFields": true,
5+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
6+
"module": "ESNext",
7+
"skipLibCheck": true,
8+
9+
/* Bundler mode */
10+
"moduleResolution": "Bundler",
11+
"allowImportingTsExtensions": true,
12+
"resolveJsonModule": true,
13+
"isolatedModules": true,
14+
"noEmit": true,
15+
"jsx": "react-jsx",
16+
17+
/* Linting */
18+
"strict": true,
19+
"noUnusedLocals": true,
20+
"noUnusedParameters": true,
21+
"noFallthroughCasesInSwitch": true
22+
},
23+
"include": ["src", "eslint.config.js"]
24+
}

examples/react/chat/vite.config.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from 'vite'
2+
import react from '@vitejs/plugin-react'
3+
import tailwindcss from '@tailwindcss/vite'
4+
5+
export default defineConfig({
6+
plugins: [tailwindcss(), react()],
7+
})

0 commit comments

Comments
 (0)