DEV Community

Cover image for Stop "Window Is Not Defined" in Next.js (2025)
Devin Rosario
Devin Rosario

Posted on

Stop "Window Is Not Defined" in Next.js (2025)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

Scenario 2: Third-Party Browser-Only Libraries

// ❌ Library expects browser environment
import BrowserChart from 'chart-library';

function Dashboard() {
  return <BrowserChart data={data} />;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

Why This Works:

typeof window
├─ Server: returns "undefined" → condition fails, code skipped
└─ Browser: returns "object" → condition passes, code runs
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
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');
Enter fullscreen mode Exit fullscreen mode

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:

  1. Check the error stack trace:
ReferenceError: window is not defined
  at MyComponent (pages/index.js:12:5)
Enter fullscreen mode Exit fullscreen mode
  1. Identify the context:
  2. Component body? → Move to useEffect
  3. Import statement? → Use dynamic import
  4. getServerSideProps/getStaticProps? → Never use window there

  5. Test your fix properly:

npm run build && npm start
# Not 'npm run dev' - dev mode masks SSR issues
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)