Onábíyí Olámídé

Frontend Engineer

Image from the movie Alien - from cosmos.com
tanstack.com/start

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.