The Server-Side Rendering Reality Check
You're building a Next.js app. Everything works perfectly in development. Then you run npm run build and suddenly:
ReferenceError: window is not defined
This error occurs because Next.js pre-renders pages using the Node.js server, and in this server environment, we don't have access to browser-specific objects like window. It's not a bug—it's a fundamental difference between server and browser JavaScript execution.
Understanding Next.js Pre-Rendering
NEXT.JS EXECUTION FLOW:
Server (Node.js) Client (Browser)
┌──────────────────┐ ┌──────────────────┐
│ ✗ No window │ │ ✓ window exists │
│ ✗ No document │ ────→ │ ✓ document exists│
│ ✗ No localStorage│ │ ✓ localStorage OK│
│ │ │ │
│ Pre-render HTML │ │ Hydration │
└──────────────────┘ └──────────────────┘
↓ ↓
ReferenceError Everything works
When Next.js pre-renders a page, it generates the HTML then sends that to the client. When a user visits the page, it loads the HTML then rehydrates by running React to make the page interactive. This is where the issue can occur.
Common Scenarios That Break
Scenario 1: Direct Window Access in Component Body
// ❌ Breaks during server-side rendering
function MyComponent() {
const width = window.innerWidth;
return <div>Width: {width}</div>;
}
Scenario 2: Third-Party Browser-Only Libraries
// ❌ Library expects browser environment
import BrowserChart from 'chart-library';
function Dashboard() {
return <BrowserChart data={data} />;
}
Scenario 3: Event Listeners at Module Level
// ❌ Executes immediately when module loads on server
window.addEventListener('resize', handleResize);
export default function App() {
return <div>App</div>;
}
Solution 1: The typeof Check Pattern
The simplest approach uses a conditional check to verify window exists before accessing it.
function MyComponent() {
if (typeof window !== 'undefined') {
// This code only runs in browser
const width = window.innerWidth;
console.log('Browser width:', width);
}
return <div>Component content</div>;
}
Why This Works:
typeof window
├─ Server: returns "undefined" → condition fails, code skipped
└─ Browser: returns "object" → condition passes, code runs
Solution 2: useEffect Hook (Recommended)
You can define a state variable and use the window event handler inside useEffect, which only runs in the browser after the component mounts.
import { useState, useEffect } from 'react';
function ResponsiveComponent() {
const [windowWidth, setWindowWidth] = useState(0);
useEffect(() => {
// ✓ Only runs in browser, never on server
setWindowWidth(window.innerWidth);
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>Window width: {windowWidth}px</div>;
}
The useEffect hook never executes during server-side rendering, making it the safest place for browser API calls.
Solution 3: Dynamic Imports with SSR Disabled
For components that absolutely require browser APIs, Next.js provides dynamic imports with an option to disable server-side rendering entirely.
import dynamic from 'next/dynamic';
// Completely skip SSR for this component
const MapViewer = dynamic(
() => import('../components/MapViewer'),
{ ssr: false }
);
export default function Page() {
return (
<div>
<h1>Interactive Map</h1>
<MapViewer />
</div>
);
}
| Solution | Best For | Performance Impact |
|---|---|---|
| typeof check | Simple window property access | None |
| useEffect | Stateful logic dependent on browser | Minimal |
| Dynamic import | Heavy browser-only libraries | Delays component render |
Real-World Example: Safe localStorage Hook
// useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(initialValue);
useEffect(() => {
// Safely access localStorage only in browser
try {
const item = window.localStorage.getItem(key);
if (item) {
setStoredValue(JSON.parse(item));
}
} catch (error) {
console.error('localStorage error:', error);
}
}, [key]);
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('localStorage error:', error);
}
};
return [storedValue, setValue];
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light');
Development teams working on mobile app development in Michigan have implemented this pattern to build fully server-rendered Next.js applications that gracefully handle browser APIs without errors.
Debugging: Finding the Error Source
When you encounter this error:
- Check the error stack trace:
ReferenceError: window is not defined
at MyComponent (pages/index.js:12:5)
- Identify the context:
- Component body? → Move to useEffect
- Import statement? → Use dynamic import
getServerSideProps/getStaticProps? → Never use window there
Test your fix properly:
npm run build && npm start
# Not 'npm run dev' - dev mode masks SSR issues
Next.js 13+ App Router Considerations
Even in client components marked with "use client", you can still encounter window is not defined errors during the initial server render. The "use client" directive doesn't eliminate server-side rendering—it just indicates the component needs client-side JavaScript.
'use client' // Still pre-renders on server!
import { useEffect, useState } from 'react';
export default function ClientComponent() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null; // Avoid hydration mismatch
// Now safe to use window
return <div>Width: {window.innerWidth}</div>;
}
Prevention Checklist
BEFORE CODING:
□ Does this need browser APIs?
□ Can I defer window access to useEffect?
□ Should I use dynamic import with ssr: false?
□ Have I tested the production build?
TESTING:
□ Run npm run build locally
□ Check server logs for errors
□ Test with JavaScript disabled
□ Verify SSR HTML output
Framework Evolution Note
The discrepancy between the server-side Node.js runtime and the client-side browser environment is fundamental. Browser-specific objects like window, document, and navigator are present in browsers but not on the server. This isn't unique to Next.js—any SSR framework (Nuxt, SvelteKit, Remix) faces the same challenge.
Key Takeaways
The "window is not defined" error is actually a feature that reminds you Next.js renders on the server first. By using useEffect for stateful logic, typeof checks for simple operations, and dynamic imports for browser-dependent libraries, you build truly universal applications.
Remember: If you can avoid using window, you probably should. Server-rendered content is faster, more accessible, better for SEO, and works without JavaScript enabled.
Top comments (0)