Live playground
All widgets import tanstack-fetch from source and use a mock fetch / stream. Use the Framework picker to show only your stack.
Open in StackBlitz
| Example | Framework | StackBlitz | Docs |
|---|---|---|---|
| TanStack Query | React | Open | React |
| Next.js SSR | React · Next.js | Open | Next SSR |
| Nuxt SSR | Vue · Nuxt | — | Nuxt SSR |
| Upload | Core demo | Open | Upload |
| SSE | React / adapters | Open | SSE |
| WebSocket · WebTransport · NDJSON | Core demo | Live on this page ↓ | Realtime |
| tRPC | React / Start | Open | tRPC recipe |
| Auth status | Core | Open | Refresh |
Status matrix (incl. 429)
Framework
Core
Live demo · real createFetch
Trigger HTTP statuses
Mock fetch returns the code you click. Handlers fire before FetchError is thrown — including 429 with Retry-After.
Tap a status — real createFetch + mock fetch
Event log
Handlers and throws show up here.
const api = createFetch({
interceptors: [
createStatusInterceptor({
401: () => logout(),
429: ({ context }) => {
const waitMs = parseRetryAfter(context.response?.headers)
// show toast / backoff…
},
'4xx': ({ status }) => console.warn('4xx', status),
}),
],
})Upload
Framework
Core
Live demo · Upload
api.upload + multipart fields
Real api.upload() builds FormData and posts through createFetch from tanstack-fetch/sse. Add onUploadProgress in your app for XHR progress.
No file selected
Pick a file and upload — real api.upload() + mock fetch
await api.upload('/files', {
file,
fieldName: 'file',
fields: { folder: 'avatars', public: true },
onUploadProgress: ({ progress }) => setProgress(progress),
})queryFn shape
Live demo · queryFn shape
Returns data · throws on error
Click Run — this calls createFetch against a mock API
const api = createFetch({ baseUrl, fetch: mockFetch })
// same shape as TanStack Query queryFn
const users = await api.get<User[]>('/users', {})
React / TanStack Query
Framework
React
Live demo · React / TanStack Query
useQuery + useMutation shape
Real createFetch calls — same queryFn / mutationFn contract your React app uses.
idle — click useQuery
const api = createFetch({ baseUrl })
// React Query
useQuery({
queryKey: ['users'],
queryFn: ({ signal }) => api.get<User[]>('/users', { signal }),
})
useMutation({
mutationFn: (body) => api.post<User>('/users', { body }),
})SSE · React useSse
Framework
React · tanstack-fetch/react
Live demo · useSse
React useSse + status
Pass {'{ client: api }'} — no FetchProvider required. Status: connecting → connected → disconnected / error.
status: disconnected
import { createFetch } from 'tanstack-fetch/sse'
import { useSse } from 'tanstack-fetch/react'
const api = createFetch({
baseUrl: import.meta.env.VITE_API_URL,
plugins: ['sse-resume'],
getToken: () => localStorage.getItem('access_token'),
})
const OrdersLive = () => {
// Pass client — FetchProvider is optional
const { data, status, error, close } = useSse<Tick>('/orders/stream', {
client: api,
})
if (status === 'error') return <p>{error?.message}</p>
return (
<div>
<p>{status}</p>
<p>{data ? `#${data.n}` : '—'}</p>
<button type="button" onClick={close}>Disconnect</button>
</div>
)
}Next.js SSR
Framework
React · Next.js
Live demo · Next.js SSR
ssr-forward cookie / auth
Real createFetch with source: 'ssr' + ssr-forward — watch headers get attached (then skipped in browser).
Request log
Run server prefetch to see forwarded Cookie / Authorization.
// app/users/page.tsx (Next.js App Router)
import { cookies } from 'next/headers'
import { createFetch } from 'tanstack-fetch'
import { createSsrForwardInterceptor, createTraceInterceptor } from 'tanstack-fetch/plugins'
const createServerApi = async () =>
createFetch({
baseUrl: process.env.API_URL!,
source: 'ssr',
interceptors: [createSsrForwardInterceptor(), createTraceInterceptor()],
incoming: async () => ({
cookie: (await cookies()).toString(),
}),
})
await queryClient.query({
queryKey: ['users'],
queryFn: async () => (await createServerApi()).get('/users'),
})Solid
Framework
Solid · tanstack-fetch/solid
Browser demos for Solid live in the Solid guide and SSE · Solid. StackBlitz apps are React today — copy the Solid snippets from those pages.
TanStack Start
Framework
TanStack Start · Query + Router context
Start + Query patterns: TanStack Query · tRPC recipe.
SSE resume (Last-Event-ID)
Framework
Core · api.sse · lastEventId
Live demo · SSE resume
Drop the connection. Lose nothing.
Real tanstack-fetch/sse client. The server is simulated in your browser: it reads Last-Event-ID and continues after it. Start the stream, cut the connection mid-way, and watch the ids.
Idle
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
import { createFetch } from 'tanstack-fetch/sse'
const api = createFetch({ baseUrl: '/api' })
let lastEventId: string | undefined
const connect = () =>
api.sse<Tick>('/events', {
lastEventId, // sent as the Last-Event-ID header
onEvent: (event) => {
lastEventId = event.id ?? lastEventId
render(event.data)
},
// mid-stream drop: reconnect from the last processed id
onError: () => setTimeout(connect, 1000),
})
connect()SSE core stream
Framework
Core · api.sse
Live demo · SSE
api.sse over fetch
Real tanstack-fetch/sse against a mock event stream — cookies / Authorization work (unlike EventSource).
Idle — start the stream
import { createFetch } from 'tanstack-fetch/sse'
import { useSse } from 'tanstack-fetch/react'
const api = createFetch({
plugins: ['sse-resume'],
getToken: () => localStorage.getItem('access_token'),
})
const OrdersLive = () => {
const { data, status, error, close } = useSse<Tick>('/orders/stream', {
client: api,
})
if (status === 'error') return <p>{error?.message}</p>
return (
<>
<p>{status} · tick #{data?.n}</p>
<button onClick={close}>Close</button>
</>
)
}WebSocket
Framework
Core · api.ws · tanstack-fetch/live
Live demo · WebSocket
useWs — auth + reconnect
Chat against a mock socket. Send before it opens (queued), then drop the connection: it reconnects and getToken() runs again, so the URL gets a fresh token.
status: disconnected
import { createFetch } from 'tanstack-fetch/live'
import { useWs } from 'tanstack-fetch/react'
const api = createFetch({ baseUrl, getToken })
const ChatRoom = ({ id }: { id: number }) => {
const { data, status, send, close } = useWs<ChatMessage, { id: number }>('/rooms/:id', {
client: api,
params: { id },
authIn: 'query', // browsers can't send WS headers
reconnect: { retries: 3, delayMs: 600 },
})
return (
<>
<p>{status} · {data?.text}</p>
<button onClick={() => send({ text: 'hi' })}>Send</button>
<button onClick={close}>Close</button>
</>
)
}WebTransport
Framework
Core · api.transport · tanstack-fetch/live
Live demo · WebTransport
useWs({ transport: 'webtransport' })
Game state streams in as unreliable datagrams (newest value wins). Same API as api.ws: send input back with send().
status: disconnected
import { useWs } from 'tanstack-fetch/react'
const Game = () => {
const { data: state, status, send } = useWs<GameState, { id: number }, Input>('/game/:id', {
client: api,
params: { id: 1 },
authIn: 'query',
transport: 'webtransport', // HTTP/3 datagrams
})
return (
<Board dot={state} live={status === 'connected'}
onBoost={() => send({ boost: 1.5 })} />
)
}NDJSON stream
Framework
Core · api.stream · tanstack-fetch/live
Live demo · NDJSON
api.stream + streamedQuery
A POST with a body and an Authorization header, read line by line. EventSource cannot do either.
status: disconnected
import {
experimental_streamedQuery as streamedQuery,
useQuery,
} from '@tanstack/react-query'
const Answer = ({ prompt }: { prompt: string }) => {
const { data: tokens = [], fetchStatus, refetch } = useQuery({
queryKey: ['chat', prompt],
queryFn: streamedQuery({
streamFn: ({ signal }) =>
api.stream<Token>('/chat', { body: { prompt }, signal }),
}),
enabled: false,
})
return (
<>
<button onClick={() => refetch()}>Ask</button>
<p>{tokens.map((t) => t.text).join('')}</p>
{fetchStatus === 'fetching' && <span>streaming…</span>}
</>
)
}