Skip to content

Commit b68ccc1

Browse files
committed
fix: restore generation hooks blog post to original content
1 parent f2ae05a commit b68ccc1

File tree

1 file changed

+44
-33
lines changed

1 file changed

+44
-33
lines changed

src/blog/generation-hooks.md

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,36 @@ authors:
77

88
![Generation Hooks](/blog-assets/generation-hooks/header.png)
99

10-
Chat is just the beginning. Most AI-powered apps need to generate images, convert text to speech, transcribe audio, summarize documents, or create videos. Until now, wiring up each of these meant custom fetch logic, manual loading state, bespoke error handling, and a different streaming protocol for every one.
10+
Chat is just the beginning. Your AI-powered app probably needs to generate images, convert text to speech, transcribe audio, summarize documents, or create videos. Until now, wiring up each of these activities meant writing custom fetch logic, managing loading states, handling errors, and juggling streaming protocols for every single one.
1111

12-
TanStack AI now ships **generation hooks**: a unified set of React hooks (with Solid, Vue, and Svelte coming soon) that give you first-class primitives for every non-chat AI activity.
12+
Not anymore.
1313

1414
## One Pattern to Rule Them All
1515

16+
TanStack AI now ships **generation hooks**: a unified set of React hooks (with Solid, Vue, and Svelte support) that give you first-class primitives for every non-chat AI activity:
17+
1618
- `useGenerateImage()` for image generation
1719
- `useGenerateSpeech()` for text-to-speech
1820
- `useTranscription()` for audio transcription
1921
- `useSummarize()` for text summarization
2022
- `useGenerateVideo()` for video generation
2123

22-
Every hook follows the same API surface. Learn one, know them all:
24+
Every hook follows the exact same API surface. Learn one, and you know them all:
2325

2426
```tsx
2527
const { generate, result, isLoading, error, stop, reset } = useGenerateImage({
2628
connection: fetchServerSentEvents('/api/generate/image'),
2729
})
2830

31+
// That's it. Call generate() and your UI reacts.
2932
generate({ prompt: 'A neon-lit cyberpunk cityscape at sunset' })
3033
```
3134

32-
`result` is fully typed. `error` is handled. Loading state is tracked. Abort is built in. No boilerplate, no `useEffect` spaghetti, no manual state management.
35+
The `result` is fully typed. The `error` is handled. Loading state is tracked. Abort is built in. No boilerplate, no `useEffect` spaghetti, no manual state management.
3336

3437
## Three Ways to Connect
3538

36-
Every generation hook supports three transport modes.
39+
Every generation hook supports three transport modes, so you can pick the one that fits your architecture:
3740

3841
### 1. Streaming (Connection Adapter)
3942

@@ -56,37 +59,37 @@ const stream = generateImage({
5659
return toServerSentEventsResponse(stream)
5760
```
5861

59-
Works with any server framework, any hosting provider, any deployment model.
62+
This is the most flexible option. It works with any server framework, any hosting provider, any deployment model.
6063

6164
### 2. Direct (Fetcher)
6265

63-
When you don't need streaming, the fetcher mode does exactly what it sounds like:
66+
Sometimes you don't need streaming. You just want to call a function and get a result. The fetcher mode does exactly that:
6467

6568
```tsx
6669
const { generate, result, isLoading } = useGenerateImage({
6770
fetcher: (input) => generateImageFn({ data: input }),
6871
})
6972
```
7073

71-
The server function runs, returns JSON, the hook updates your UI. Simple, synchronous from the user's perspective, fully type-safe.
74+
The server function runs, returns JSON, and the hook updates your UI. Simple, synchronous from the user's perspective, and fully type-safe.
7275

73-
### 3. Server Function Streaming
76+
### 3. Server Function Streaming (NEW)
7477

75-
This is the one we're most excited about. It combines the type safety of server functions with the real-time feedback of streaming, and it works beautifully with TanStack Start.
78+
This is the one we're most excited about. It combines the **type safety of server functions** with the **real-time feedback of streaming**, and it works beautifully with TanStack Start.
7679

77-
The problem: the `connection` approach uses a generic `Record<string, any>` for its data payload. Your input loses all type information. The `fetcher` approach is fully typed, but blocks until the entire result is ready.
80+
Here is the problem we solved: the `connection` approach uses a generic `Record<string, any>` for its data payload. Great for flexibility, but your input loses all type information. The `fetcher` approach is fully typed, but it waits for the entire result before updating the UI.
7881

79-
Server Function Streaming gives you both. Your fetcher returns a `Response` (an SSE stream), and the client detects it automatically and parses the stream in real-time:
82+
Server Function Streaming gives you both. Your fetcher returns a `Response` object (an SSE stream), and the client automatically detects it and parses the stream in real-time:
8083

8184
```tsx
82-
// Client - identical API to a direct fetcher
85+
// Client - looks identical to the direct fetcher
8386
const { generate, result, isLoading } = useGenerateImage({
8487
fetcher: (input) => generateImageStreamFn({ data: input }),
8588
})
8689
```
8790

8891
```typescript
89-
// Server - add stream: true, wrap with toServerSentEventsResponse
92+
// Server - just add stream: true and wrap with toServerSentEventsResponse
9093
export const generateImageStreamFn = createServerFn({ method: 'POST' })
9194
.inputValidator(
9295
z.object({
@@ -106,9 +109,9 @@ export const generateImageStreamFn = createServerFn({ method: 'POST' })
106109
)
107110
```
108111

109-
From the client's perspective the API is identical to a direct fetcher call. Behind the scenes, TanStack AI detects the `Response` object, reads the SSE stream, and feeds chunks through the same event pipeline as the connection adapter. Progress events fire in real-time. Errors are reported as they happen. Your `input` parameter stays fully typed throughout.
112+
From the client's perspective, the API is identical to a direct fetcher call. But behind the scenes, TanStack AI detects the `Response` object, reads the SSE stream, and feeds chunks through the same event pipeline used by the connection adapter. Progress events fire in real-time. Errors are reported as they happen. And your `input` parameter stays fully typed throughout.
110113

111-
Zero config. If your fetcher returns a `Response`, it's treated as an SSE stream. If it returns anything else, it's a direct result.
114+
The detection is simple and zero-config: if your fetcher returns a `Response`, it's treated as an SSE stream. If it returns anything else, it's treated as a direct result. No flags, no configuration, no separate hook.
112115

113116
## How It Works Under the Hood
114117

@@ -118,24 +121,27 @@ When a fetcher returns a `Response`, the `GenerationClient` runs a simple check:
118121
const result = await this.fetcher(input, { signal })
119122

120123
if (result instanceof Response) {
121-
// Parse as SSE stream same pipeline as ConnectionAdapter
124+
// Parse as SSE stream - same pipeline as ConnectionAdapter
122125
await this.processStream(parseSSEResponse(result, signal))
123126
} else {
124127
// Use as direct result
125128
this.setResult(result)
126129
}
127130
```
128131

129-
`parseSSEResponse` reads the response body as a stream of newline-delimited SSE events, parses each `data:` line into a `StreamChunk`, and yields them into the same `processStream` method that the ConnectionAdapter uses. Same event types, same state transitions, same callbacks. Every feature that works with streaming connections works with server function streaming: progress reporting, chunk callbacks, abort signals, error handling.
132+
The `parseSSEResponse` utility reads the response body as a stream of newline-delimited SSE events, parses each `data:` line into a `StreamChunk`, and yields them into the same `processStream` method that the ConnectionAdapter uses. Same event types, same state transitions, same callbacks.
133+
134+
This means every feature that works with streaming connections also works with server function streaming: progress reporting, chunk callbacks, abort signals, error handling. All of it.
130135

131136
## Result Transforms
132137

133-
Sometimes the raw result from the server isn't what you want in state. Every generation hook accepts an `onResult` callback that transforms the result before it's stored:
138+
Sometimes the raw result from the server isn't what you want to store in state. Every generation hook accepts an `onResult` callback that can transform the result before it's stored:
134139

135140
```tsx
136141
const { result } = useGenerateSpeech({
137142
fetcher: (input) => generateSpeechStreamFn({ data: input }),
138143
onResult: (raw) => {
144+
// Convert base64 audio to a blob URL for playback
139145
const bytes = Uint8Array.from(atob(raw.audio), (c) => c.charCodeAt(0))
140146
const blob = new Blob([bytes], { type: raw.contentType ?? 'audio/mpeg' })
141147
return {
@@ -149,11 +155,11 @@ const { result } = useGenerateSpeech({
149155
// result is typed as { audioUrl: string; format?: string; duration?: number } | null
150156
```
151157

152-
TypeScript infers the output type from your transform. No explicit generics needed.
158+
TypeScript infers the output type from your transform function. No explicit generics needed.
153159

154-
## Video Generation
160+
## Video Generation: A First-Class Citizen
155161

156-
Video generation is a different beast. Providers like OpenAI's Sora use a jobs-based architecture: submit a prompt, receive a job ID, poll for status until the video is ready. This can take minutes.
162+
Video generation is a different beast. Unlike image or speech generation, video providers like OpenAI's Sora use a jobs-based architecture: you submit a prompt, receive a job ID, then poll for status until the video is ready. This can take minutes.
157163

158164
`useGenerateVideo()` handles all of this transparently:
159165

@@ -162,6 +168,7 @@ const { generate, result, jobId, videoStatus, isLoading } = useGenerateVideo({
162168
fetcher: (input) => generateVideoStreamFn({ data: input }),
163169
})
164170

171+
// In your JSX:
165172
{
166173
videoStatus && (
167174
<div>
@@ -177,10 +184,12 @@ const { generate, result, jobId, videoStatus, isLoading } = useGenerateVideo({
177184
}
178185
```
179186

180-
`jobId` and `videoStatus` are reactive state that update in real-time as the server streams polling updates. Your users see "pending", "processing", progress percentages, and finally the completed video URL. You write zero polling loops.
187+
The hook exposes `jobId` and `videoStatus` as reactive state that updates in real-time as the server streams polling updates. Your users see "pending", "processing", progress percentages, and finally the completed video URL, all without you writing a single polling loop.
181188

182189
## Every Activity, Same API
183190

191+
Here's what makes this design special: the API is identical across all five generation types. Once you've built an image generation page, building a speech generation page is a matter of swapping the hook name and adjusting the input:
192+
184193
| Hook | Input | Result |
185194
| --------------------- | ------------------------------------ | ----------------------------------------------- |
186195
| `useGenerateImage()` | `{ prompt, numberOfImages?, size? }` | `{ images: [{ url, b64Json, revisedPrompt }] }` |
@@ -189,15 +198,17 @@ const { generate, result, jobId, videoStatus, isLoading } = useGenerateVideo({
189198
| `useSummarize()` | `{ text, style?, maxLength? }` | `{ summary }` |
190199
| `useGenerateVideo()` | `{ prompt, size?, duration? }` | `{ jobId, status, url }` |
191200

192-
Same `generate()`. Same `result`. Same `isLoading`. Same `error`. Same `stop()` and `reset()`. The consistency is intentional: AI features should be as easy to add to your app as a form submission.
201+
Same `generate()`. Same `result`. Same `isLoading`. Same `error`. Same `stop()` and `reset()`. The consistency is intentional: we want AI features to be as easy to add to your app as a form submission.
193202

194203
## Getting Started
195204

205+
Install the packages:
206+
196207
```bash
197208
pnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-client @tanstack/ai-openai
198209
```
199210

200-
Create a streaming server function:
211+
Create a server function that streams:
201212

202213
```typescript
203214
import { createServerFn } from '@tanstack/react-start'
@@ -206,15 +217,15 @@ import { openaiImage } from '@tanstack/ai-openai'
206217

207218
export const generateImageStreamFn = createServerFn({ method: 'POST' })
208219
.inputValidator(z.object({ prompt: z.string() }))
209-
.handler(({ data }) =>
210-
toServerSentEventsResponse(
220+
.handler(({ data }) => {
221+
return toServerSentEventsResponse(
211222
generateImage({
212223
adapter: openaiImage('gpt-image-1'),
213224
prompt: data.prompt,
214225
stream: true,
215226
}),
216-
),
217-
)
227+
)
228+
})
218229
```
219230

220231
Use it in your component:
@@ -241,12 +252,12 @@ function ImageGenerator() {
241252
}
242253
```
243254

244-
Three lines of hook setup. Type-safe input. Streaming progress. Error handling. Abort support.
255+
Three lines of hook setup. Type-safe input. Streaming progress. Error handling. Abort support. That's it.
245256

246257
## What's Next
247258

248-
Generation hooks are available now in `@tanstack/ai-client` and `@tanstack/ai-react`. Solid, Vue, and Svelte support is coming with the same API surface.
259+
Generation hooks are available now in `@tanstack/ai-client` and `@tanstack/ai-react`. Support for Solid, Vue, and Svelte is coming soon with the same API surface.
249260

250-
We're also expanding the adapter ecosystem so you can use these hooks with providers beyond OpenAI. The generation functions are provider-agnostic by design, so swapping from OpenAI to Anthropic or a local model is a single line change.
261+
We're also working on expanding the adapter ecosystem so you can use these hooks with providers beyond OpenAI. The generation functions are provider-agnostic by design, so swapping from OpenAI to Anthropic or a local model will be a single line change.
251262

252-
Build something and let us know what you make.
263+
Build something cool and let us know. We can't wait to see what you create.

0 commit comments

Comments
 (0)