fatline

TanStack Query

Preserve types through SSR hydration

TanStack Query's SSR hydration serializes your query cache to JSON and back. Without fatline, your Date, Set, and Map values become strings and arrays.

The Problem

// Server: Query returns Date
const { data } = useQuery({
  queryKey: ['user'],
  queryFn: () => ({ createdAt: new Date() })
})

// After SSR hydration:
data.createdAt instanceof Date  // false ← it's a string now

Setup

import { serialize, deserialize } from 'fatline'
import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    dehydrate: {
      serializeData: serialize,
    },
    hydrate: {
      deserializeData: deserialize,
    },
  },
})

Now your types survive hydration:

data.createdAt instanceof Date  // true

Next.js App Router

// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { serialize, deserialize } from 'fatline'
import { useState } from 'react'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000,
      },
      dehydrate: {
        serializeData: serialize,
      },
      hydrate: {
        deserializeData: deserialize,
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined

function getQueryClient() {
  if (typeof window === 'undefined') {
    return makeQueryClient()
  }
  return (browserQueryClient ??= makeQueryClient())
}

export function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient()

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}
// app/layout.tsx
import { Providers } from './providers'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

With Prefetching

// app/users/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import { serialize, deserialize } from 'fatline'
import { UserList } from './user-list'

export default async function UsersPage() {
  const queryClient = new QueryClient({
    defaultOptions: {
      dehydrate: { serializeData: serialize },
      hydrate: { deserializeData: deserialize },
    },
  })

  await queryClient.prefetchQuery({
    queryKey: ['users'],
    queryFn: getUsers,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <UserList />
    </HydrationBoundary>
  )
}

With tRPC

If you're using tRPC with the fatline transformer, types are already preserved through tRPC's serialization. You only need this setup if you're using TanStack Query directly without tRPC.

// tRPC already handles serialization
import { transformer } from 'fatline/trpc'

const t = initTRPC.create({ transformer })

Streaming SSR

fatline works with TanStack Query's streaming SSR. Pending queries are dehydrated with their data intact:

// Pending queries serialize correctly
await queryClient.prefetchQuery({
  queryKey: ['users'],
  queryFn: getUsers,
})

// Even if the query hasn't resolved yet, the dehydrated state
// will serialize any partial data correctly
const dehydratedState = dehydrate(queryClient)

Next Steps

On this page