Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Streaming SSR and Hydration Mismatches

Hello! Welcome back to our course on mastering TanStack Start.

In our last lesson, we built a progressively enhanced form, a powerful pattern that ensures your application is both robust and user-friendly. We saw how server functions act as the bridge to our backend, while TanStack Form enhances the client-side experience.

Today, we'll zoom out from a single component to the entire page-rendering process. The learning outcome for this lesson is to configure streaming SSR for progressive page rendering and handle potential hydration mismatches. Mastering these two concepts is essential for building high-performance, stable SSR applications and is the final step in our module on migrating a client-side SPA.

1. The Power of Streaming SSR

In a traditional SPA, the user receives a minimal HTML file and must wait for a large JavaScript bundle to download and execute before seeing any meaningful content. Classic SSR improves on this by sending a fully rendered HTML page, but the user still has to wait for the entire page to be generated on the server, which can be slow if it involves multiple data fetches.

Streaming SSR offers the best of both worlds. The server sends an initial HTML shell almost immediately, which the browser can start rendering. Then, as slower parts of the page (like data-dependent components) are ready, the server streams the additional HTML and data to the client in the same request. This dramatically improves the perceived performance and user experience.

TanStack Router and Start are built with this modern approach in mind.

Next Gen Fullstack React with TanStack

Tanner Lindsley, the creator of TanStack, briefly discusses how streaming is a core feature of TanStack Router itself. This provides some high-level context before we dive into the implementation details.

Watch from 00:10:58 to 00:11:34. Note his mention of "streaming streamed hydration" and "deferred promise streaming" as capabilities that don't even require the full TanStack Start framework, highlighting how fundamental this pattern is.

2. Implementing Streaming SSR

Enabling streaming involves two key pieces: telling React which parts of your UI can be streamed, and configuring the server to handle a streaming response.

Step 1: Using Suspense to Mark Streamable Boundaries

The primary tool for enabling streaming is React's <Suspense> component. When you wrap a component in <Suspense>, you're telling React: "This component might take a while to load. Render the fallback UI for now, and stream the actual component's HTML once it's ready."

In a data-fetching context with TanStack Query, this is often as simple as switching from useQuery to useSuspenseQuery.

SSR, Preloading, Caching and more with TanStack Start + Query

This video provides an excellent practical demonstration of converting a client-side rendered component to a streaming server-rendered one.

Watch from 00:00:27 to 00:02:34. Pay close attention to: The initial state with useQuery, where skeletons are shown on the client. The switch to useSuspenseQuery and wrapping the component in <Suspense>. The network tab in the browser's developer tools, where you can see the document request remains 'pending' as content is streamed in. The page becomes interactive even while the server is still sending data.

As you saw, the code changes in the component are minimal:

  1. Replace useQuery with useSuspenseQuery. This hook integrates with React Suspense, meaning it will throw a promise during data fetching, which signals to a parent <Suspense> boundary to show its fallback.
  2. Remove the isLoading state, as useSuspenseQuery guarantees that data is available by the time the component renders.
  3. Wrap the component that uses useSuspenseQuery in a <Suspense> component in its parent.
// Before: Client-side loading
const { data, isLoading } = useQuery(...);
if (isLoading) return <Skeletons />;
return <MyComponent data={data} />;

// After: SSR Streaming
// In the component:
const { data } = useSuspenseQuery(...);
return <MyComponent data={data} />;

// In the parent component:
<Suspense fallback={<Skeletons />}>
  <MyDataComponent />
</Suspense>

Step 2: Configuring the Server for Streaming

The final piece is to tell your server entry point to use a streaming handler. TanStack Start makes this straightforward.

SSR | TanStack Router React Docs

The official TanStack Router documentation explains the server-side configuration. Let's look at how to switch from a non-streaming to a streaming setup.

Read the section titled "Streaming SSR". Notice the two options: defaultStreamHandler and renderRouterToStream. For TanStack Start projects, you'll typically be modifying your entry-server.tsx to use one of these.

In a typical TanStack Start project, your entry-server.tsx file will handle incoming requests. To enable streaming, you simply use the defaultStreamHandler provided by the framework.

// src/entry-server.tsx
import {
  createRequestHandler,
  // You switch from defaultRenderToString to defaultStreamHandler
  defaultStreamHandler,
} from '@tanstack/react-router/server';
import { createRouter } from './router';

// This function is called by your server (e.g., Vinxi/Nitro)
export default createRequestHandler({
  createRouter,
  // The key change is here!
  render: defaultStreamHandler,
});

With Suspense in your components and the defaultStreamHandler on your server, you have successfully configured streaming SSR.

3. The Challenge of Hydration

Hydration is the process where client-side React "takes over" the static HTML sent by the server, attaching event listeners and making the page interactive. For this to work, the initial UI that React renders on the client must be identical to the HTML that the server produced.

A hydration mismatch (or error) occurs when they are not identical. React will log a warning, and in the worst case, it can lead to a broken UI or unpredictable behavior because the client and server have a different understanding of the application's state.

Common causes include:

  • Using browser-only APIs like window or document in code that runs on the server.
  • Rendering content based on values that differ between server and client (e.g., new Date(), Math.random()).
  • Rendering different content based on a user's locale or timezone, which the server may not know on the initial request.

4. Strategies for Resolving Hydration Mismatches

When migrating an SPA, you'll almost certainly encounter hydration mismatches. Here are the canonical strategies for fixing them, from most to least preferred.

Hydration Errors | TanStack Start React Docs

This documentation page from TanStack Start is your go-to guide for this topic. It outlines several effective strategies for debugging and resolving these errors.

Read through all the strategies on this page. We will discuss each one, but having the official text and code examples as a reference is invaluable.

Let's walk through the strategies from the documentation.

Strategy 1 & 2: Make Server and Client Match

The best solution is to ensure both environments render the same thing. This often involves the client "telling" the server about its environment so that subsequent server-renders are correct.

A common example is the user's timezone.

  1. Server-Side (First Visit): The server doesn't know the user's timezone, so it renders times in a default (e.g., 'UTC'). It can guess the locale from the accept-language header.
  2. Client-Side: A small client-side component runs in a useEffect, gets the actual timezone (Intl.DateTimeFormat().resolvedOptions().timeZone), and saves it to a cookie.
  3. Server-Side (Subsequent Visits): The server now receives the timezone cookie with the request. It can use this value to render dates and times correctly, matching what the client would render. The hydration mismatch is gone.

The code examples for localeTzMiddleware and SetTimeZoneCookie in the "Hydration Errors" document illustrate this pattern perfectly.

Strategy 3: Render Client-Only Components

Some components are simply not meant for SSR, especially those that heavily manipulate the DOM or rely on browser APIs (e.g., complex charting libraries, rich text editors). For these, the solution is to prevent them from rendering on the server altogether.

TanStack Router provides a <ClientOnly> utility for this.

import { ClientOnly } from '@tanstack/react-router'

// This component uses the 'window' object and cannot be server-rendered.
import MyComplexChart from './MyComplexChart'

function MyDashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <ClientOnly fallback={<div>Loading chart...</div>}>
        {/* This will only be rendered on the client after hydration */}
        <MyComplexChart />
      </ClientOnly>
    </div>
  )
}

The server will render the fallback, and the client will render the same fallback during its initial render. Then, in an effect, it will swap in the actual <MyComplexChart /> component. This avoids a mismatch.

Strategy 4: Disable SSR for a Specific Route

If an entire page is problematic or provides little value from being server-rendered, you can disable SSR at the route level.

// src/routes/some-client-heavy-route.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/some-client-heavy-route')({
  // Option 1: No SSR at all for this route's component.
  // The server sends an empty shell.
  ssr: false,

  // Option 2: Only run the loader on the server, but don't render the component.
  // Useful if you need server-fetched data for a client-rendered component.
  // ssr: 'data-only',

  component: ExpensiveClientComponent,
})

Strategy 5: Suppress the Warning (Last Resort)

If a mismatch is minor, unavoidable, and you've confirmed it causes no actual bugs (e.g., a timestamp that's off by a few milliseconds), you can use React's suppressHydrationWarning prop as a last resort.

<span suppressHydrationWarning>
  {new Date().toISOString()}
</span>

Use this sparingly, as it can hide real problems.

Conclusion

Congratulations on completing Module 2! You've tackled the core challenges of migrating a client-side application to a modern SSR framework.

Key Takeaways:

  • Streaming SSR improves perceived performance by sending HTML to the browser incrementally. It's enabled by using <Suspense> in your components and a streaming handler (defaultStreamHandler) on the server.
  • Hydration Mismatches occur when server-rendered HTML differs from the initial client-render. They are a common issue in SSR that must be resolved for application stability.
  • You have a toolkit of strategies to fix hydration errors: synchronize client/server state with cookies, use <ClientOnly> for browser-dependent components, disable SSR for specific routes, or (rarely) suppress the warning.

You now have a solid foundation in rendering and structuring a TanStack Start application. In our next module, we will shift our focus to a critical aspect of any real-world app: authentication. We'll begin by learning how to store a JWT securely in an HTTP-only cookie upon login, the first step in building a secure SSR application.

Can't find a good explanation? Sign up and we'll make it for you

Sign up