Skip to content

Commit 42500ce

Browse files
examples: Add a new TanStack Start example for typed readable streams (#5363)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b9f2313 commit 42500ce

File tree

12 files changed

+516
-2
lines changed

12 files changed

+516
-2
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
title: Streaming Data from Server Functions
3+
---
4+
5+
Streaming data from the server has become very popular thanks to the rise of AI apps. Luckily, it's a pretty easy task with TanStack Start, and what's even better: the streamed data is typed!
6+
7+
The two most popular ways of streaming data from server functions are using `ReadableStream`-s or async generators.
8+
9+
You can see how to implement both in the [Streaming Data From Server Functions example](https://github.com/TanStack/router/tree/main/examples/react/start-streaming-data-from-server-functions).
10+
11+
## Typed Readable Streams
12+
13+
Here's an example for a server function that streams an array of messages to the client in a type-safe manner:
14+
15+
```ts
16+
type Message = {
17+
content: string
18+
}
19+
20+
/**
21+
This server function returns a `ReadableStream`
22+
that streams `Message` chunks to the client.
23+
*/
24+
const streamingResponseFn = createServerFn().handler(async () => {
25+
// These are the messages that you want to send as chunks to the client
26+
const messages: Message[] = generateMessages()
27+
28+
// This `ReadableStream` is typed, so each
29+
// will be of type `Message`.
30+
const stream = new ReadableStream<Message>({
31+
async start(controller) {
32+
for (const message of messages) {
33+
// Send the message
34+
controller.enqueue(message)
35+
}
36+
controller.close()
37+
},
38+
})
39+
40+
return stream
41+
})
42+
```
43+
44+
When you consume this stream from the client, the streamed chunks will be properly typed:
45+
46+
```ts
47+
const [message, setMessage] = useState('')
48+
49+
const getTypedReadableStreamResponse = useCallback(async () => {
50+
const response = await streamingResponseFn()
51+
52+
if (!response) {
53+
return
54+
}
55+
56+
const reader = response.getReader()
57+
let done = false
58+
while (!done) {
59+
const { value, done: doneReading } = await reader.read()
60+
done = doneReading
61+
if (value) {
62+
// Notice how we know the value of `chunk` (`Message | undefined`)
63+
// here, because it's coming from the typed `ReadableStream`
64+
const chunk = value.content
65+
setMessage((prev) => prev + chunk)
66+
}
67+
}
68+
}, [])
69+
```
70+
71+
## Async Generators in Server Functions
72+
73+
A much cleaner approach with the same results is to use an async generator function:
74+
75+
```ts
76+
const streamingWithAnAsyncGeneratorFn = createServerFn().handler(
77+
async function* () {
78+
const messages: Message[] = generateMessages()
79+
for (const msg of messages) {
80+
await sleep(500)
81+
// The streamed chunks are still typed as `Message`
82+
yield msg
83+
}
84+
},
85+
)
86+
```
87+
88+
The client side code will also be leaner:
89+
90+
```ts
91+
const getResponseFromTheAsyncGenerator = useCallback(async () => {
92+
for await (const msg of await streamingWithAnAsyncGeneratorFn()) {
93+
const chunk = msg.content
94+
setMessages((prev) => prev + chunk)
95+
}
96+
}, [])
97+
```

docs/start/framework/react/server-functions.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,13 @@ Access request headers, cookies, and response customization:
204204
- `setResponseHeader()` - Set custom response headers
205205
- `setResponseStatus()` - Custom status codes
206206

207-
### Streaming & Raw Responses
207+
### Streaming
208208

209-
Return `Response` objects for streaming, binary data, or custom content types.
209+
Stream typed data from server functions to the client. See the [Streaming Data from Server Functions guide](../guide/streaming-data-from-server-functions).
210+
211+
### Raw Responses
212+
213+
Return `Response` objects binary data, or custom content types.
210214

211215
### Progressive Enhancement
212216

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "tanstack-start-streaming-data-from-server-functions",
3+
"private": true,
4+
"sideEffects": false,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite dev",
8+
"build": "vite build && tsc --noEmit",
9+
"start": "vite start"
10+
},
11+
"dependencies": {
12+
"@tanstack/react-router": "^1.132.33",
13+
"@tanstack/react-router-devtools": "^1.132.33",
14+
"@tanstack/react-start": "^1.132.36",
15+
"react": "^19.0.0",
16+
"react-dom": "^19.0.0",
17+
"zod": "^3.24.2"
18+
},
19+
"devDependencies": {
20+
"@types/node": "^22.5.4",
21+
"@types/react": "^19.0.8",
22+
"@types/react-dom": "^19.0.3",
23+
"@vitejs/plugin-react": "^4.3.4",
24+
"typescript": "^5.7.2",
25+
"vite": "^7.1.7",
26+
"vite-tsconfig-paths": "^5.1.4"
27+
}
28+
}
15 KB
Binary file not shown.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* eslint-disable */
2+
3+
// @ts-nocheck
4+
5+
// noinspection JSUnusedGlobalSymbols
6+
7+
// This file was automatically generated by TanStack Router.
8+
// You should NOT make any changes in this file as it will be overwritten.
9+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10+
11+
import { Route as rootRouteImport } from './routes/__root'
12+
import { Route as IndexRouteImport } from './routes/index'
13+
14+
const IndexRoute = IndexRouteImport.update({
15+
id: '/',
16+
path: '/',
17+
getParentRoute: () => rootRouteImport,
18+
} as any)
19+
20+
export interface FileRoutesByFullPath {
21+
'/': typeof IndexRoute
22+
}
23+
export interface FileRoutesByTo {
24+
'/': typeof IndexRoute
25+
}
26+
export interface FileRoutesById {
27+
__root__: typeof rootRouteImport
28+
'/': typeof IndexRoute
29+
}
30+
export interface FileRouteTypes {
31+
fileRoutesByFullPath: FileRoutesByFullPath
32+
fullPaths: '/'
33+
fileRoutesByTo: FileRoutesByTo
34+
to: '/'
35+
id: '__root__' | '/'
36+
fileRoutesById: FileRoutesById
37+
}
38+
export interface RootRouteChildren {
39+
IndexRoute: typeof IndexRoute
40+
}
41+
42+
declare module '@tanstack/react-router' {
43+
interface FileRoutesByPath {
44+
'/': {
45+
id: '/'
46+
path: '/'
47+
fullPath: '/'
48+
preLoaderRoute: typeof IndexRouteImport
49+
parentRoute: typeof rootRouteImport
50+
}
51+
}
52+
}
53+
54+
const rootRouteChildren: RootRouteChildren = {
55+
IndexRoute: IndexRoute,
56+
}
57+
export const routeTree = rootRouteImport
58+
._addFileChildren(rootRouteChildren)
59+
._addFileTypes<FileRouteTypes>()
60+
61+
import type { getRouter } from './router.tsx'
62+
import type { createStart } from '@tanstack/react-start'
63+
declare module '@tanstack/react-start' {
64+
interface Register {
65+
ssr: true
66+
router: Awaited<ReturnType<typeof getRouter>>
67+
}
68+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createRouter } from '@tanstack/react-router'
2+
import { routeTree } from './routeTree.gen'
3+
4+
export function getRouter() {
5+
const router = createRouter({
6+
routeTree,
7+
defaultPreload: 'intent',
8+
defaultErrorComponent: (err) => <p>{err.error.stack}</p>,
9+
defaultNotFoundComponent: () => <p>not found</p>,
10+
scrollRestoration: true,
11+
})
12+
13+
return router
14+
}
15+
16+
declare module '@tanstack/react-router' {
17+
interface Register {
18+
router: ReturnType<typeof getRouter>
19+
}
20+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/// <reference types="vite/client" />
2+
import * as React from 'react'
3+
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
4+
import {
5+
HeadContent,
6+
Link,
7+
Outlet,
8+
Scripts,
9+
createRootRoute,
10+
} from '@tanstack/react-router'
11+
import appCss from '~/styles/app.css?url'
12+
13+
export const Route = createRootRoute({
14+
head: () => ({
15+
links: [{ rel: 'stylesheet', href: appCss }],
16+
}),
17+
component: RootComponent,
18+
})
19+
20+
function RootComponent() {
21+
return (
22+
<RootDocument>
23+
<Outlet />
24+
</RootDocument>
25+
)
26+
}
27+
28+
function RootDocument({ children }: { children: React.ReactNode }) {
29+
return (
30+
<html>
31+
<head>
32+
<HeadContent />
33+
</head>
34+
<body>
35+
{children}
36+
<TanStackRouterDevtools position="bottom-right" />
37+
<Scripts />
38+
</body>
39+
</html>
40+
)
41+
}

0 commit comments

Comments
 (0)