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

Browser API Isolation for Server-Side Safety

Hello! Welcome back to our course on migrating a Single-Page Application to TanStack Start.

In our previous lesson, we took a major step by refactoring data fetching from client-side useEffect hooks to server-side route loaders. This is fantastic for performance, but it introduces a new challenge. Because our components and loaders now run on the server during the initial render, any code that relies on browser-specific APIs—like window, document, or localStorage—will crash the server process.

Today's lesson directly addresses this critical issue. Our learning outcome is to isolate and handle browser-specific APIs (e.g., window, document) to prevent server-side errors. We will explore a hierarchy of strategies, from simple conditional checks to the powerful, framework-specific tools that TanStack Start provides.

1. The Core Problem: "Window is not defined"

Before diving into solutions, it's essential to understand why this error occurs in an SSR environment. Your code is now executing in two distinct places: a Node.js environment on the server and the user's browser on the client. The Node.js environment has no concept of a window or a document; those are browser-specific global objects.

Even though you might be working in a React component, SSR frameworks pre-render that component on the server to generate the initial HTML. If the component's render logic tries to access window.innerWidth, for example, the Node.js process will throw a ReferenceError: window is not defined.

This short video explains the concept clearly. While it uses Next.js for its examples, the underlying principle is universal to all SSR frameworks, including TanStack Start.

Next.js Error: window is not defined (FIXED)

To start, watch this clip from ByteGrad's video "Next.js Error: window is not defined (FIXED)". It provides a concise explanation of why this error happens during server pre-rendering.

Watch the first minute (00:00 - 01:07). Focus on the explanation that even client components are still pre-rendered on the server, which is the root cause of the issue.

TanStack Start's philosophy makes this even more pertinent. Let's quickly review its core execution model.

Execution Model | TanStack Start React Docs

The TanStack Start documentation on the "Execution Model" clarifies its foundational principle.

Please read the sections "Core Principle: Isomorphic by Default" and "The Execution Boundary". This will reinforce that unless you specify otherwise, your code is intended to run in both server and client environments.

With this "isomorphic by default" model, we need explicit strategies to control where our code executes.

2. Traditional Solutions: Conditional Checks and useEffect

Before we get to TanStack's specific APIs, let's cover the fundamental React patterns for solving this. These are still valuable tools in your arsenal.

  1. The typeof window Guard: A simple if statement to check if the window object exists before trying to access it.
  2. The useEffect Hook: Since useEffect (with an empty dependency array) runs only on the client after the component has mounted, it's a safe place for any browser-specific logic.

The same video from ByteGrad demonstrates these two patterns effectively.

Next.js Error: window is not defined (FIXED)

This next segment demonstrates the two most common, framework-agnostic solutions to the 'window is not defined' error.

Watch from 01:17 to 02:40. You'll see demonstrations of both the typeof window !== 'undefined' check and the useEffect hook pattern.

Here's a quick example combining these ideas:

import { useState, useEffect } from 'react';

function ResponsiveComponent() {
  // State to hold the window width. Default to a server-safe value.
  const [width, setWidth] = useState(0);

  useEffect(() => {
    // This code only runs on the client.
    function handleResize() {
      setWidth(window.innerWidth);
    }
    
    // Set the initial width.
    handleResize();
    
    window.addEventListener('resize', handleResize);
    
    // Cleanup function.
    return () => window.removeEventListener('resize', handleResize);
  }, []); // Empty dependency array ensures it runs only once on mount.

  // On the server, width will be 0. On the client, it will update.
  // This can cause a hydration mismatch if not handled carefully.
  if (width === 0) {
    return <div>Loading dimensions...</div>; // Server-rendered fallback
  }

  return <div>Window width is: {width}px</div>;
}

While effective, these manual checks can become cumbersome. TanStack Start provides more declarative and robust APIs to handle these scenarios.

3. TanStack's API-Driven Solutions

TanStack offers a suite of tools designed specifically for managing execution environments.

For Components: The <ClientOnly> Wrapper

This is your primary tool for any component that must not be rendered on the server. A common use case is a component that uses a charting library, interacts with localStorage, or directly manipulates the DOM.

The <ClientOnly> component from @tanstack/react-router will render a fallback UI on the server and then render its children on the client after hydration is complete.

This video provides a perfect, practical demonstration of encountering a localStorage error and fixing it with <ClientOnly>.

Getting Started with TanStack Start RC

In this clip from "Getting Started with TanStack Start RC", Maximilian Schwarzmüller runs into the exact problem we're discussing and solves it using <ClientOnly>.

Watch from 01:39:08 to 01:42:58. Observe how the application crashes when trying to access localStorage during SSR and how wrapping the component in <ClientOnly> resolves the issue.

For Functions: createClientOnlyFn and createIsomorphicFn

What if you need to isolate a utility function rather than an entire component?

  • createClientOnlyFn: Use this for functions that should only ever run on the client. If called on the server, it will throw an error, making bugs obvious.
  • createIsomorphicFn: This is a more flexible tool that allows you to provide different implementations for the server and the client.

The official documentation provides clear examples of these APIs.

Execution Model | TanStack Start React Docs

Let's return to the "Execution Model" documentation to see the APIs for client-only and isomorphic functions.

Read the sections "Client-Only Execution", "Environment-Specific Implementations", and "Manual vs API-Driven Environment Detection". This will show you how to use <ClientOnly>, createClientOnlyFn, and createIsomorphicFn, and how they compare to manual checks.

Here's a practical example of createIsomorphicFn:

import { createIsomorphicFn } from '@tanstack/react-start';

// This function can be called anywhere, and the correct implementation will be used.
const getSessionId = createIsomorphicFn()
  .client(() => {
    // On the client, get it from localStorage.
    return localStorage.getItem('sessionId');
  })
  .server(() => {
    // On the server, you might get it from request headers or a cookie.
    // (Note: Accessing request-specific data on the server is more complex
    // and often done within loaders or server functions, but this illustrates the concept).
    return 'server-session-placeholder';
  });

// Usage in a component:
function UserProfile() {
  const sessionId = getSessionId();
  // ...
}

4. The "Nuclear Option": Disabling SSR for a Route

Sometimes, a route is so heavily dependent on browser APIs (e.g., a page with a complex WebGL canvas) that trying to patch it is impractical. In these cases, you can disable SSR for the entire route.

TanStack Router allows you to set ssr: false in your route definition. This tells TanStack Start not to run the route's loader or render its component on the server for the initial request. The browser will receive a placeholder and then render the route entirely on the client, just like in a traditional SPA.

Selective Server-Side Rendering (SSR)

The "Selective Server-Side Rendering (SSR)" documentation explains how to opt-out of SSR on a per-route basis.

Read the sections "What is Selective SSR?" and "ssr: false". This will explain the use case and show the simple configuration needed to disable SSR for a specific route.

Example:

// src/routes/my-canvas-editor.tsx
import { createFileRoute } from '@tanstack/react-router';
import { HeavyClientComponent } from '../components/HeavyClientComponent';

export const Route = createFileRoute('/my-canvas-editor')({
  // This entire route will now render only on the client
  ssr: false,
  component: HeavyClientComponent,
});

5. A Decision Framework

You now have a range of tools to handle browser-specific code. Which one should you choose? The documentation provides an excellent decision framework.

Execution Model | TanStack Start React Docs

To tie everything together, let's review the recommended architectural patterns.

Read the "Architecture Decision Framework" section. It gives clear guidelines on when to choose Server-Only, Client-Only, or Isomorphic execution.

Here is a summary of the client-side strategies:

Situation Recommended Strategy Why?
A component uses a client-side library (e.g., D3.js, Leaflet). Wrap the component in <ClientOnly>. Prevents server rendering, allows a fallback, and isolates the dependency.
A utility function needs to access localStorage or navigator. Use createClientOnlyFn. Creates a clear, type-safe boundary for client-only logic.
A function needs to behave differently on server vs. client. Use createIsomorphicFn. Provides a single, elegant API for environment-aware logic.
A small part of a component needs to know the window size. Use useEffect to set state. A simple, localized solution for dynamic UI that doesn't need a wrapper.
An entire page is fundamentally incompatible with SSR. Set ssr: false on the route definition. The most direct way to revert a specific page to SPA behavior.

Conclusion

Mastering the execution boundary is fundamental to building robust SSR applications. Today, we've seen how to identify and resolve errors caused by browser-specific APIs being called on the server.

Key Takeaways:

  • The Problem: SSR executes code in a Node.js environment which lacks browser globals like window and document, causing errors.
  • The <ClientOnly> Wrapper: Your go-to solution for wrapping components that rely on browser APIs, preventing them from rendering on the server.
  • API-Driven Functions: Use createClientOnlyFn for logic that only runs on the client and createIsomorphicFn for logic that has different server and client implementations.
  • Selective SSR: For routes that are heavily client-dependent, you can use the ssr: false option to opt-out of server rendering entirely for that page.
  • Fundamental Patterns: Simple useEffect hooks and typeof window checks remain useful for smaller, localized client-side logic within a component.

In our next lesson, we will continue our migration journey by focusing on the application's structure. We will learn how to refactor an existing client-side router (like React Router DOM) to use TanStack's file-based routing conventions, bringing our project's structure in line with the TanStack Start paradigm.

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

Sign up