Recommended resources
The freely available resources this lesson will direct you to.
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:
- Replace
useQuerywithuseSuspenseQuery. 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. - Remove the
isLoadingstate, asuseSuspenseQueryguarantees thatdatais available by the time the component renders. - Wrap the component that uses
useSuspenseQueryin 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
windowordocumentin 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.
- 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-languageheader. - 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. - 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.