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
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
title: Streaming Data from Server Functions
---

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!

The two most popular ways of streaming data from server functions are using `ReadableStream`-s or async generators.

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).

## Typed Readable Streams

Here's an example for a server function that streams an array of messages to the client in a type-safe manner:

```ts
type Message = {
content: string
}

/**
This server function returns a `ReadableStream`
that streams `Message` chunks to the client.
*/
const streamingResponseFn = createServerFn().handler(async () => {
// These are the messages that you want to send as chunks to the client
const messages: Message[] = generateMessages()

// This `ReadableStream` is typed, so each
// will be of type `Message`.
const stream = new ReadableStream<Message>({
async start(controller) {
for (const message of messages) {
// Send the message
controller.enqueue(message)
}
controller.close()
},
})

return stream
})
```

When you consume this stream from the client, the streamed chunks will be properly typed:

```ts
const [message, setMessage] = useState('')

const getTypedReadableStreamResponse = useCallback(async () => {
const response = await streamingResponseFn()

if (!response) {
return
}

const reader = response.getReader()
let done = false
while (!done) {
const { value, done: doneReading } = await reader.read()
done = doneReading
if (value) {
// Notice how we know the value of `chunk` (`Message | undefined`)
// here, because it's coming from the typed `ReadableStream`
const chunk = value.content
setMessage((prev) => prev + chunk)
}
}
}, [])
```

## Async Generators in Server Functions

A much cleaner approach with the same results is to use an async generator function:

```ts
const streamingWithAnAsyncGeneratorFn = createServerFn().handler(
async function* () {
const messages: Message[] = generateMessages()
for (const msg of messages) {
await sleep(500)
// The streamed chunks are still typed as `Message`
yield msg
}
},
)
```

The client side code will also be leaner:

```ts
const getResponseFromTheAsyncGenerator = useCallback(async () => {
for await (const msg of await streamingWithAnAsyncGeneratorFn()) {
const chunk = msg.content
setMessages((prev) => prev + chunk)
}
}, [])
```
8 changes: 6 additions & 2 deletions docs/start/framework/react/server-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,13 @@ Access request headers, cookies, and response customization:
- `setResponseHeader()` - Set custom response headers
- `setResponseStatus()` - Custom status codes

### Streaming & Raw Responses
### Streaming

Return `Response` objects for streaming, binary data, or custom content types.
Stream typed data from server functions to the client. See the [Streaming Data from Server Functions guide](../guide/streaming-data-from-server-functions).

### Raw Responses

Return `Response` objects binary data, or custom content types.

### Progressive Enhancement

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "tanstack-start-streaming-data-from-server-functions",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build && tsc --noEmit",
"start": "vite start"
},
Comment on lines +6 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the Vite start script

vite start is not a valid CLI entry point in Vite 7, so npm run start will fail. Switch to vite preview instead.

   "scripts": {
     "dev": "vite dev",
     "build": "vite build && tsc --noEmit",
-    "start": "vite start"
+    "start": "vite preview"
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"scripts": {
"dev": "vite dev",
"build": "vite build && tsc --noEmit",
"start": "vite start"
},
"scripts": {
"dev": "vite dev",
"build": "vite build && tsc --noEmit",
"start": "vite preview"
},
🤖 Prompt for AI Agents
examples/react/start-streaming-data-from-server-functions/package.json lines
6-10: the "start" script uses the invalid CLI command "vite start" (Vite v7);
change it to use "vite preview" instead so npm run start runs the correct Vite
preview server after build.

"dependencies": {
"@tanstack/react-router": "^1.132.33",
"@tanstack/react-router-devtools": "^1.132.33",
"@tanstack/react-start": "^1.132.36",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^3.24.2"
},
Comment on lines +12 to +18
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use workspace protocol for internal TanStack deps

Per the repository guidelines, internal TanStack packages in examples should reference local workspaces to avoid mismatched registry installs.

-    "@tanstack/react-router": "^1.132.33",
-    "@tanstack/react-router-devtools": "^1.132.33",
-    "@tanstack/react-start": "^1.132.36",
+    "@tanstack/react-router": "workspace:*",
+    "@tanstack/react-router-devtools": "workspace:*",
+    "@tanstack/react-start": "workspace:*",

As per coding guidelines.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@tanstack/react-router": "^1.132.33",
"@tanstack/react-router-devtools": "^1.132.33",
"@tanstack/react-start": "^1.132.36",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^3.24.2"
},
"dependencies": {
"@tanstack/react-router": "workspace:*",
"@tanstack/react-router-devtools": "workspace:*",
"@tanstack/react-start": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^3.24.2"
},
🤖 Prompt for AI Agents
In examples/react/start-streaming-data-from-server-functions/package.json around
lines 12 to 18, the three internal TanStack deps (@tanstack/react-router,
@tanstack/react-router-devtools, @tanstack/react-start) are pinned to registry
versions; change them to use the workspace protocol (e.g. "workspace:*") so the
example resolves local monorepo packages instead of installing mismatched
registry versions, leave react, react-dom and zod unchanged, and run
install/lockfile update afterwards.

Copy link
Contributor

@schiller-manuel schiller-manuel Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai all our examples use the versioned packages instead of local references since we need those examples to be runnable on its own (e.g. when using gitpick)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i mistyped, i meant you @coderabbitai

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@schiller-manuel, thank you for the clarification! I apologize for the incorrect suggestion. You're absolutely right that examples need to use versioned packages to remain runnable as standalone projects (e.g., via gitpick).

I have a learning in my knowledge base that suggests using workspace:* protocol for internal dependencies in package.json files, but it doesn't account for examples. Would you like me to:

  1. Remove that learning entirely, or
  2. Keep it but add a new learning that examples should use versioned packages instead of workspace protocol?

I want to make sure I get this right for future reviews!


🧠 Learnings used
Learnt from: CR
PR: TanStack/router#0
File: AGENTS.md:0-0
Timestamp: 2025-09-23T17:36:12.598Z
Learning: Applies to **/package.json : Use workspace:* protocol for internal dependencies in package.json files

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep it but add a new learning that examples should use versioned packages instead of workspace protocol?

"devDependencies": {
"@types/node": "^22.5.4",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4"
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-disable */

// @ts-nocheck

// noinspection JSUnusedGlobalSymbols

// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'

const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function getRouter() {
const router = createRouter({
routeTree,
defaultPreload: 'intent',
defaultErrorComponent: (err) => <p>{err.error.stack}</p>,
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider security implications of displaying raw error stack.

Displaying err.error.stack directly in the UI could expose sensitive information (file paths, internal structure, etc.) in production. For a demo/example this is acceptable, but consider adding a comment noting this should be replaced with a user-friendly error message in production.

Apply this diff to add a clarifying comment:

-    defaultErrorComponent: (err) => <p>{err.error.stack}</p>,
+    // Note: In production, avoid displaying raw stack traces to users
+    defaultErrorComponent: (err) => <p>{err.error.stack}</p>,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
defaultErrorComponent: (err) => <p>{err.error.stack}</p>,
// Note: In production, avoid displaying raw stack traces to users
defaultErrorComponent: (err) => <p>{err.error.stack}</p>,
🤖 Prompt for AI Agents
In examples/react/start-streaming-data-from-server-functions/src/router.tsx
around line 8, the defaultErrorComponent currently renders err.error.stack
directly which can leak sensitive internals; add an inline comment above or next
to this line noting that showing raw error stacks is only acceptable in demos
and must be replaced in production with a generic/user-friendly message (and
consider logging the full stack server-side instead).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai this is just a demo, not a production app, so ignore this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@schiller-manuel, you're absolutely right! I understand this is a demo example, and displaying the raw error stack is perfectly appropriate for educational purposes. I'll make a note of this for future reviews of example code. Thank you for the clarification!


✏️ Learnings added
Learnt from: schiller-manuel
PR: TanStack/router#5363
File: examples/react/start-streaming-data-from-server-functions/src/router.tsx:8-8
Timestamp: 2025-10-05T20:43:54.644Z
Learning: For code in example or demo directories (e.g., paths containing `examples/`), do not flag production-level security concerns such as displaying raw error stacks, hardcoded credentials for demos, or lack of input validation. Examples are for educational purposes and different standards apply.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

defaultNotFoundComponent: () => <p>not found</p>,
scrollRestoration: true,
})

return router
}

declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/// <reference types="vite/client" />
import * as React from 'react'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import {
HeadContent,
Link,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/react-router'
import appCss from '~/styles/app.css?url'

export const Route = createRootRoute({
head: () => ({
links: [{ rel: 'stylesheet', href: appCss }],
}),
component: RootComponent,
})

function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
)
}

function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<TanStackRouterDevtools position="bottom-right" />
<Scripts />
</body>
</html>
)
}
Loading
Loading