
Implementing Out of Order Streaming with Tanstack Start and Tanstack Query
You have to first setup a new Tanstack start project with Tanstack Query you can use
npx create-tsrouter-app@latest --add-ons
and select Tanstack React Query and Tanstack Start and it handles the setup for you.
So now let's create a new route Tanstack Start uses file based routing there is also an option for virtual file routes that look like this
// routes.ts
import {
rootRoute,
route,
index,
layout,
physical,
} from '@tanstack/virtual-file-routes'
export const routes = rootRoute('root.tsx', [
index('index.tsx'),
layout('pathlessLayout.tsx', [
route('/dashboard', 'app/dashboard.tsx', [
index('app/dashboard-index.tsx'),
route('/invoices', 'app/dashboard-invoices.tsx', [
index('app/invoices-index.tsx'),
route('$id', 'app/invoice-detail.tsx'),
]),
]),
physical('/posts', 'posts'),
]),
])
You don't lose any typesafety using this by the way.
For this tutorial I will assume we are using file based routing for now.
In your routes folder create a new file called about.tsx
if you have you dev server running you should see something created for you
// about.tsx
export const Route = createFileRoute("/about")({
component: RouteComponent,
});
function RouteComponent() {
return (
<div>
Home Component
</div>
);
}
Now let's create a two different server functions we will simulate a 5s delay in the first one and a 1s delay in the second one
// getServerTime.ts
import { createServerFn } from '@tanstack/react-start'
export const getServerTime = createServerFn().handler(async () => {
// Wait for 1 second
await new Promise((resolve) => setTimeout(resolve, 5000))
// Return the current time
return new Date().toISOString()
})
// getUser.ts
export const getUser = createServerFn().handler(async () => {
// Wait for 5 seconds
await new Promise(resolve => setTimeout(resolve, 5000))
return {
name: Olamide,
age: 25,
isActive: true
}
})
// query-options.ts
export const getServerTimeQueryOptions = () => queryOptions({
queryKey: ['get-server-time'],
queryFn: () => getServerTime()
})
export const getUserQueryOptions = () => queryOptions({
queryKey: ['get-user'],
queryFn: () => getUser()
})
// about.tsx
export const Route = createFileRoute("/about")({
loader: async ({ context }) => {
context.queryClient.ensureQueryData(getServerTimeQueryOptions())
context.queryClient.ensureQueryData(getUserQueryOptions())
},
component: RouteComponent,
})
function RouteComponent() {
return (
<div className="space-y-8 p-4">
<h1 className="text-2xl font-bold">About Page - Streaming Demo</h1>
<TimeComponent />
<UserComponent />
</div>
)
}
Notice that we don't await the ensureQueryData
calls. This initiates the requests on the server without delaying rendering, allowing the rest of the page to render immediately. Any component using these queries via useSuspenseQuery
will suspend and show a fallback until the data becomes available. If the data fetching results in an error, it will be caught by the nearest error boundary.
Create two different components and use the queryOptions via useSuspenseQuery
and see how the two components render at different times and while fetching it displays the fallback
// TimeComponent.tsx
import { useSuspenseQuery } from '@tanstack/react-query'
import { getServerTimeQueryOptions } from './query-options'
import { Suspense } from 'react'
function TimeDisplay() {
const { data } = useSuspenseQuery(getServerTimeQueryOptions())
return <div>Server time: {data}</div>
}
export function TimeComponent() {
return (
<Suspense fallback={<div>Loading server time...</div>}>
<TimeDisplay />
</Suspense>
)
}
// UserComponent.tsx
import { useSuspenseQuery } from '@tanstack/react-query'
import { getUserQueryOptions } from './query-options'
import { Suspense } from 'react'
function UserDisplay() {
const { data } = useSuspenseQuery(getUserQueryOptions())
return (
<div>
<h2>User Info</h2>
<p>Name: {data.name}</p>
<p>Age: {data.age}</p>
<p>Status: {data.isActive ? 'Active' : 'Inactive'}</p>
</div>
)
}
export function UserComponent() {
return (
<Suspense fallback={<div>Loading user data...</div>}>
<UserDisplay />
</Suspense>
)
}
When you visit this page, you'll see the user data appears after about 1 second, while the server time takes around 5 seconds to load. This demonstrates the power of out-of-order streaming - each piece of UI can load independently as its data becomes available, rather than waiting for all data to be ready.