Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to the next stage of your journey.
In the previous module, we built a solid foundation for a server-rendered application, culminating in creating robust, server-side protected routes. We now have the core skills to build a secure SSR app from scratch.
This module, "Migration Strategy: From SPA to SSR," shifts our focus to your primary goal: transforming an existing Single-Page Application. This is where the theory meets practice. Our first step is a critical reconnaissance mission. Before we can change a single line of code, we must understand the landscape of the existing application and identify what will break in a server environment.
This lesson addresses the first, crucial learning outcome of this module:
Analyze an existing SPA to identify code dependent on client-side browser APIs (e.g., window, localStorage).
As a seasoned front-end developer, you've spent years leveraging the power of the browser. Now, we'll learn to view that same code through the lens of SSR, identifying which parts are fundamentally tied to the client and will require refactoring.
1. The Fundamental Conflict: Browser vs. Server Environment
A Single-Page Application runs entirely within a user's browser. A Server-Side Rendered application has a dual-execution model: the initial page render happens on a server (like a Node.js or Bun process), and then the application "hydrates" and takes over on the client, behaving like an SPA from that point on.
The problem arises because the server environment is not a browser. It's a JavaScript runtime that lacks the entire suite of browser-specific APIs. The global object on a server is not window; it's global (in Node.js) or a similar construct, and it does not contain objects like document, localStorage, or location.
To quickly ground this concept, let's review what the window object represents.
What is the 'window' Object (JavaScript)
This short video from ByteGrad, 'What is the 'window' Object (JavaScript)', serves as a concise reminder of the window object's role as the global, top-level object specifically within a browser context.
From the beginning to about two minutes in, watch the window object introduction. Focus on how it describes window as the global object for the browser and how APIs like document, location, and fetch are properties of it. The key takeaway is that window is intrinsically tied to a browser tab.
Attempting to access window or any of its properties directly in code that runs during server-side rendering will result in a ReferenceError. This will crash the rendering process for that page. Your first task in any migration is to hunt down every piece of code that makes this assumption.
2. The "Most Wanted" List: Common Client-Side APIs
When auditing an SPA, you should be searching for a specific set of keywords. These are the most common browser-only APIs that cause issues with SSR.
Here is a watchlist to guide your search:
window: The global browser object.- Properties:
window.location,window.innerWidth,window.innerHeight,window.scrollY. - Methods:
window.alert(),window.confirm(),window.open(). - Event Listeners:
window.addEventListener('resize', ...),window.addEventListener('scroll', ...).
- Properties:
document: The API for interacting with the DOM.- Properties:
document.cookie. - Methods:
document.getElementById(),document.querySelector(),document.addEventListener(). Direct DOM manipulation is an anti-pattern in React, but it can appear, especially when integrating with third-party libraries.
- Properties:
localStorage&sessionStorage: The Web Storage API, used for client-side data persistence.navigator: Provides information about the browser and system.- Properties:
navigator.userAgent,navigator.language,navigator.onLine. - APIs:
navigator.geolocation,navigator.clipboard.
- Properties:
fetch: While modern Node.js includes afetchimplementation, its context is different. Code that relies on browser-managed cookies being sent automatically withfetchwill behave differently on the server. We will cover this nuance in more detail later. For now, consider anyfetchcall a point of interest.
3. Case Study: A Common SPA Pattern
One of the most frequent uses of client-side APIs is persisting state. Let's analyze a typical example of using localStorage to save a user's preference.
Save State to LocalStorage & Persist on Refresh with React.js
The video 'Save State to LocalStorage & Persist on Refresh with React.js' by Colby Fayock demonstrates a classic SPA pattern. It's an excellent example of the kind of code we need to identify and eventually refactor.
Watch the section covering using useEffect to both save state to and retrieve state from localStorage. As you watch, try to pinpoint the exact lines of code that would fail if this component were rendered on a server.
After watching the video, consider this: if you tried to server-render the component shown, your application would crash. The calls to window.localStorage.setItem(...) and window.localStorage.getItem(...) are the culprits. Even though they are inside a useEffect hook (which is designed to run client-side after the initial render), some SSR frameworks or patterns might behave unexpectedly. More importantly, any logic that tries to read from localStorage to determine the initial state of a component will definitely run on the server and cause a ReferenceError.
4. Auditing Strategies and Recognizing "Guards"
Now that you know what to look for, how do you find it in a large codebase?
-
Global Code Search: Use your IDE's global search feature (or a command-line tool like
greporripgrep) to search for every term on the "Most Wanted" list. This is your first-pass, brute-force method to map out all potential problem areas. -
Manual Inspection: Pay special attention to:
- Custom Hooks: Hooks like
useLocalStorage,useAuth, oruseThemeare common places to find this logic. - Utility Files: Helper functions that might interact with the browser.
- Top-level Component Logic: Any code that runs during the component's render phase.
- Third-Party Libraries: Check the documentation for any libraries you use (especially for things like charting, analytics, or animations) to see if they have SSR compatibility notes.
- Custom Hooks: Hooks like
As you search, you will likely encounter code that already attempts to protect against SSR errors. It's crucial to recognize these patterns.
Mastering localStorage & sessionStorage in React + Next.js ...
This article, 'Mastering localStorage & sessionStorage in React + Next.js', provides clear examples of the most common defensive pattern used to make client-side code 'safe' for SSR environments.
Read the sections 'What are localStorage and sessionStorage?' (focusing on the core definitions), 'Using Storage in React + Next.js' (reading the usage note), 'Example 1: Persisting Theme Preference', the custom hook in 'Example 3', and the 'Gotchas & Best Practices' (reviewing the practices list). Focus on the repeated use of the typeof window !== 'undefined' check. This is the key pattern to identify.
The typeof window !== 'undefined' check is the most common way to "guard" a piece of code, ensuring it only executes in a browser environment. When you find this, it's a good sign that a developer was aware of SSR issues. However, in our migration, simply guarding the code is not always enough. Our goal is often to replace the client-side logic with a true server-side equivalent (like using cookies instead of localStorage for session data).
5. A Framework for Thinking About Solutions
When you identify a piece of client-dependent code, it will ultimately need to be handled. A post on Stack Overflow provides a useful mental model for the strategies you can employ, which we will explore in subsequent lessons.
react sever-side rendering dealing with `window`, ` ...
This Stack Overflow answer provides a high-level conceptual framework for making 'universal' code that runs in both client and server environments.
Read the answer labeled '1 Answer 1'. It outlines three approaches: 'Use existing Interface', 'Interface emulation', and 'Ignoring code execution'.
Let's briefly connect these concepts to what we've seen:
- Ignoring code execution: This is precisely what the
typeof window !== 'undefined'guard does. It tells the server to simply skip that block of code. This is a valid strategy for logic that is purely presentational and client-only (e.g., triggering an animation on scroll). - Interface emulation: This involves using a library like
JSDOMto create a fake browser environment on the server. This is a heavy-handed approach and is generally avoided in modern SSR frameworks. - Use existing Interface: This is the most powerful strategy for migration. It involves replacing a browser-only API with an alternative that works on both server and client. A prime example is replacing
localStoragefor authentication with HTTP-only cookies, which are accessible on the server during rendering and are handled by the browser automatically on the client.
For this lesson, your job is simply to be the detective—to find the clues. In the lessons that follow, we'll act on them using these strategies.
Conclusion
You are now equipped to perform the first and most critical step of an SPA-to-SSR migration: a thorough audit of client-side dependencies. This analysis forms the blueprint for the entire refactoring process.
Key Takeaways:
- SSR code runs in a non-browser environment where APIs like
windowandlocalStoragedo not exist and will cause fatalReferenceErrors. - Your first task is to audit the SPA codebase, searching for a "watchlist" of common browser APIs (
window,document,localStorage,navigator, etc.). - A common defensive pattern is the
typeof window !== 'undefined'check. Identifying this guard is as important as identifying the unguarded API calls. - The goal of the audit is to create a map of all code that needs to be either guarded, refactored, or replaced with a server-compatible alternative.
Next Up:
With a map of our client-side dependencies in hand, the next logical step is to plan the new structure of our application. In the next lesson, we will tackle: Map the route structure of a client-side router (e.g., React Router) to Tanstack Router's file-based conventions.