Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our course on migrating your React SPA to a modern SSR application with Tanstack Start.
In our previous lesson, we built a foundational toolkit for handling client-side dependencies. We learned how to use useEffect, createClientOnlyFunction, React.lazy, and the ssr: false route option to control where and when different parts of our code execute.
Today, we will apply that knowledge to one of the most common and critical tasks in an SPA-to-SSR migration. Many third-party libraries you rely on were built for a client-only world and can cause significant issues—from outright crashes on the server to subtle hydration errors on the client.
This lesson is dedicated to the learning outcome: Assess third-party library dependencies for SSR compatibility and isolate incompatible libraries. We will establish a systematic workflow to audit your dependencies, diagnose problems, and apply the isolation strategies we've learned to make them work harmoniously in an SSR environment.
1. Identifying Incompatible Libraries
Before you can fix a problematic library, you need to find it. As an experienced developer, you know that issues can manifest in different ways. In an SSR context, they typically fall into two categories.
A. Server-Side Crashes
This is the most obvious sign of incompatibility. When you start your development server or refresh a page, your application crashes, and you see an error in your terminal (the server environment).
The classic error message is:ReferenceError: window is not defined
You might also see document is not defined, localStorage is not defined, or navigator is not defined. This happens when a library's code is imported and executed by the server and immediately tries to access a browser-only API.
B. Client-Side Hydration Mismatches
This is a more subtle issue. The server successfully renders the page and sends HTML to the browser. However, when React attempts to "hydrate" the static HTML by attaching event listeners and creating the component tree in the browser, it detects a difference between the server-rendered HTML and the client-rendered output.
You'll see a warning in your browser's developer console like:Warning: An error occurred during hydration. The server HTML was replaced with client content.
Or:Warning: Text content did not match. Server: "X" Client: "Y"
This often happens with libraries that:
- Render differently based on the user's timezone (e.g., date formatters).
- Use random value generators (e.g., for unique IDs).
- Measure browser window or element dimensions on the initial render.
This video provides a great overview of how third-party components can cause these exact hydration errors.
Next.js Hydration failed because the initial ui does not match what was rendered on the server (FIX)
Watch this clip from the video 'Next.js Hydration failed...' by ByteGrad. Although it's demonstrated in a Next.js context, the problem of third-party components accessing the window object and causing mismatches is universal to all React SSR frameworks.
Watch the section from 02:34 to 03:35. It clearly explains how a third-party component can be incompatible with SSR.
2. A Toolkit of Isolation Strategies
Once you've identified a problematic library, you can apply one of several strategies. These build directly on the techniques from our last lesson, but we'll frame them here as a clear decision-making process.
Strategy 1: Conditional Rendering with useEffect
This is your primary tool for fixing hydration mismatches. If a component can be imported on the server but renders differently on the client, you can prevent it from rendering on the server altogether and let it render only after the component mounts in the browser.
The pattern involves using useState and useEffect to track whether the component has mounted on the client.
Next.js Hydration failed because the initial ui does not match what was rendered on the server (FIX)
The same video demonstrates this exact technique. Pay close attention to how useState and useEffect are combined to create a client-side-only render.
Watch from 03:27 to 05:17. The key takeaway is the isClient state variable that gates the rendering of the incompatible component.
Here's how you can encapsulate this logic into a clean, reusable wrapper component—a pattern you'll find very useful in a large application.
app/components/ClientOnly.tsx
import { useState, useEffect } from 'react';
interface ClientOnlyProps {
children: React.ReactNode;
fallback?: React.ReactNode;
}
export function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
if (!hasMounted) {
return fallback;
}
return <>{children}</>;
}
You can then wrap any incompatible component with it:
import { ClientOnly } from '~/components/ClientOnly';
import { SomeChartingLibrary } from 'some-chart-library';
function Dashboard() {
return (
<div>
<h1>My Dashboard</h1>
<ClientOnly fallback={<div>Loading chart...</div>}>
{/* This will only render on the client, avoiding hydration mismatches */}
<SomeChartingLibrary />
</ClientOnly>
</div>
);
}
Strategy 2: Dynamic Imports with React.lazy
This is the solution for server-side crashes. If a library fails with a ReferenceError the moment you import it, the useEffect trick won't work because the module itself can't be loaded on the server.
Here, you must prevent the import statement from ever running on the server. We do this using React.lazy and a dynamic import(), which we covered in the last lesson.
How to Implement Server-Side Rendering (SSR) in React
This article, 'How to Implement Server-Side Rendering (SSR) in React', discusses this exact problem. It shows how to use dynamic imports to load a component only on the client-side.
Read the section 'Handling Libraries with Browser-Specific Code' and its sub-section 'Dynamic Imports'. It uses Next.js's dynamic function, but the core concept is what matters: dynamic(() => import('../components/ClientOnlyComponent'), { ssr: false }). This is conceptually the same as using React.lazy to defer loading and then using our ClientOnly wrapper to prevent server-rendering.
Combining React.lazy with our ClientOnly wrapper from Strategy 1 gives you a robust solution:
import React, { Suspense } from 'react';
import { ClientOnly } from '~/components/ClientOnly';
// This component crashes on the server, so we lazy-load it.
const VeryIncompatibleMap = React.lazy(() => import('some-map-library'));
function ContactPage() {
return (
<div>
<h2>Our Location</h2>
<ClientOnly fallback={<div>Loading map...</div>}>
{/* Suspense is still needed for the code-splitting aspect of React.lazy */}
<Suspense fallback={<div>Loading map...</div>}>
<VeryIncompatibleMap />
</Suspense>
</ClientOnly>
</div>
);
}
In this setup:
- On the server,
ClientOnlyrenders itsfallback. TheVeryIncompatibleMapcomponent and its problematicimportare never touched. - On the client,
ClientOnlyinitially renders thefallback. - After mounting, it attempts to render its children.
Suspensethen takes over, showing its own fallback while the code forVeryIncompatibleMapis fetched from the network. - Once loaded, the map component renders.
Strategy 3: The Last Resort - suppressHydrationWarning
Sometimes, a mismatch is intentional or unavoidable. A common example is displaying a timestamp that you explicitly want to show in the user's local time. The server will render it in its timezone (e.g., UTC), and the client will render it in the user's timezone (e.g., PST), causing a mismatch.
In these specific, controlled situations, you can tell React to ignore the mismatch for a particular element.
Next.js Hydration failed because the initial ui does not match what was rendered on the server (FIX)
Let's return to the ByteGrad video one last time to see how this works and when to use it.
Watch the segment from 07:18 to 08:28. Note the emphasis that this is not an optimal solution and should be used as a last resort when you understand exactly why the warning is happening.
Example:
function MyComponent() {
// This will be different on server and client.
const time = new Date().toLocaleTimeString();
return (
<span suppressHydrationWarning={true}>
Current time: {time}
</span>
);
}
Use this sparingly. It's a signal that you're deliberately breaking the hydration contract. Always add a code comment explaining why it's necessary.
3. A Systematic Workflow for Migration
When migrating your SPA, you can follow this workflow to tackle third-party dependencies:
- Audit your
package.json: Before you even start, look through your dependencies. For any library that renders UI (sliders, charts, maps, date pickers), check its documentation for "SSR," "Next.js," or "server-side rendering" support. This can help you anticipate problems. - Run the App: Start your dev server and navigate to pages that use third-party components.
- Check the Server Console: Look for
ReferenceErrorcrashes. If you find one:- Trace it to the
importstatement. - Apply Strategy 2: Wrap the component in
React.lazyand theClientOnlywrapper.
- Trace it to the
- Check the Browser Console: If the server renders without crashing, look for hydration warnings in the browser. If you find one:
- Identify the component causing the mismatch.
- Apply Strategy 1: Wrap the component in the
ClientOnlywrapper.
- Handle Benign Mismatches: If the mismatch is known, unavoidable, and acceptable (like a local timestamp), apply Strategy 3: Use
suppressHydrationWarningand add a comment.
By following this process, you can methodically work through your application, ensuring every third-party library behaves correctly in your new SSR architecture.
Conclusion
Successfully integrating third-party libraries is a major milestone in any SSR migration. While it can seem daunting, a systematic approach makes it manageable. By identifying whether a library causes a server crash or a client hydration mismatch, you can choose the correct isolation strategy.
Key Takeaways:
- Incompatible libraries manifest as either server-side
ReferenceErrors or client-side hydration mismatches. - For server crashes, use
React.lazycombined with a client-only wrapper to prevent the module from ever being imported on the server. - For hydration mismatches, use a
ClientOnlywrapper component (based onuseStateanduseEffect) to ensure the component only renders in the browser. suppressHydrationWarningis a tool for rare, specific cases where a mismatch is unavoidable and understood, not a general-purpose fix.
Next Up:
Now that we have full control over what gets rendered on the server, we can explore another powerful rendering strategy. In the next lesson, we will learn how to configure static site generation (SSG) for specific routes within the Tanstack Start application. This will allow us to pre-render pages at build time, offering the fastest possible experience for content that doesn't change often.