Window is not defined in Next.js – 2026 Fix for React Apps
Developer Guide

Window is not defined in Next.js – 2026 Fix for React Apps

Seeing 'ReferenceError: window is not defined' in a Next.js React app? Follow this step‑by‑step guide to understand the cause and apply a concise fix that works in both server and client environments.

2026-06-03
7 min read
Window is not defined in Next.js – 2026 Fix for React Apps

TL;DR#

If you're seeing ReferenceError: window is not defined, the cause is that the code runs during server‑side rendering where window doesn't exist. Fix it by guarding the reference or moving the code into a client‑only context.

If that doesn't work, scroll to verify the fix — there are two common variants this guide also covers.

What you'll see#

ReferenceError: window is not defined
    at Object.<anonymous> (/src/components/Map.tsx:12:15)
    at Module._compile (node:internal/modules/cjs/loader:1126:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
    at Module.load (node:internal/modules/cjs/loader:1004:32)
    at Function.Module._load (node:internal/modules/cjs/loader:839:12)
    at next/dist/build/webpack/loaders/next-client-pages-loader.js:274:23

It happens when you import a component that touches window (for example a map library, a charting library, or a custom script) and then run npm run dev or deploy the app to Vercel. The behavior is the same across local development, preview deployments, and production builds.

Root cause#

Next.js executes every page component on the server for the initial request. During that phase the JavaScript environment is Node.js, which does not provide a window global. Any top‑level reference to window—whether you call window.innerWidth, window.location, or a library that accesses document—is evaluated as soon as the module is imported. Because the import happens before the component is rendered, the server throws a ReferenceError and the build fails.

The problem is amplified when you use third‑party libraries that assume a browser context. For instance, many map or chart libraries read window.devicePixelRatio at import time. If you bundle them directly in a page component, Next.js will try to evaluate that code on the server, triggering the error.

The relevant code path is:

js
// src/components/Map.tsx
import React from 'react';
import L from 'leaflet'; // leaflets reads window at import time

export default function Map() {
  const map = L.map('map'); // <-- window is accessed here
  return <div id="map" style= />;
}

When the server imports leaflet, the library immediately accesses window, causing the crash.

The fix#

There are three idiomatic ways to keep the window reference out of the server bundle:

  1. Guard the reference with typeof window !== 'undefined'.
  2. Move the code into a useEffect hook so it only runs after the component mounts in the browser.
  3. Load the library dynamically with next/dynamic and disable SSR for that component.

Below is the minimal change that resolves the error for most cases. I chose the dynamic import approach because it isolates the entire component from server rendering, keeping the bundle size small and avoiding any accidental window access.

js
// src/components/Map.tsx
import React from 'react';
import dynamic from 'next/dynamic';

// Dynamically import the map component without SSR
const LeafletMap = dynamic(() => import('../lib/LeafletMap'), {
  ssr: false,
  loading: () => <p>Loading map…</p>,
});

export default function Map() {
  return (
    <div style=>
      <LeafletMap />
    </div>
  );
}

And the dynamically loaded module:

js
// src/lib/LeafletMap.tsx
import React, { useEffect, useRef } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';

export default function LeafletMap() {
  const mapContainer = useRef(null);

  useEffect(() => {
    if (!mapContainer.current) return;

    const map = L.map(mapContainer.current).setView([51.505, -0.09], 13);
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: '&copy; OpenStreetMap contributors',
    }).addTo(map);
  }, []);

  return <div ref={mapContainer} style= />;
}

That single change addresses the cause because the leaflet import now happens only in the browser, after the server has already sent the HTML. The ssr: false flag tells Next.js to skip this component during the server pass, eliminating the window reference entirely.

Step by step#

  1. Create a new file src/lib/LeafletMap.tsx (or whatever library you’re using) and paste the second snippet above.

  2. Replace the original import in src/components/Map.tsx with the dynamic import shown in the first snippet.

  3. Remove any top‑level window usage from the original component. If you still need a small check, wrap it:

    js
    if (typeof window !== 'undefined') {
      // browser‑only code
    }
    
  4. Save the files and restart the dev server (npm run dev). The error should disappear.

If you prefer a guard instead of dynamic import, the pattern looks like this:

js
// src/components/Chart.tsx
import React, { useEffect } from 'react';
import ChartJS from 'chart.js';

export default function Chart() {
  useEffect(() => {
    if (typeof window === 'undefined') return; // guard

    const ctx = document.getElementById('myChart');
    new ChartJS(ctx, {
      type: 'bar',
      data: { /* … */ },
    });
  }, []);

  return <canvas id="myChart" />;
}

Both approaches are valid; pick the one that matches your project's style.

Verify the fix#

Run the development server again:

bash
npm run dev

You should see the usual Next.js startup log without any ReferenceError:

text
> next dev
ready - started server on http://localhost:3000

Open http://localhost:3000 in a browser. The page that previously crashed now renders the map (or chart) correctly. No red error overlay appears in the console, and the network tab shows the dynamic chunk (LeafletMap.js) loading only after the initial HTML.

If the error persists, double‑check that all imports of the offending library are now behind a guard or dynamic import. A stray import L from 'leaflet' in another component will still trigger the same crash.

Variant A — window used inside a utility module#

Sometimes the window reference lives in a helper file that is imported by many components:

js
// src/utils/browserOnly.ts
export const isMobile = () => window.innerWidth < 768;

In that case, the guard must be inside the utility:

js
// src/utils/browserOnly.ts
export const isMobile = () => {
  if (typeof window === 'undefined') return false;
  return window.innerWidth < 768;
};

Or you can export a no‑op fallback for the server:

js
export const isMobile = typeof window !== 'undefined'
  ? () => window.innerWidth < 768
  : () => false;

Variant B — Third‑party library that cannot be lazy‑loaded#

Some libraries (e.g., react-helmet-async) need to be present during SSR but still touch window internally. The fix is to patch the library with a small shim:

js
// src/shims/windowShim.js
if (typeof window === 'undefined') {
  global.window = {} as any;
}

Add this shim as the first import in next.config.js:

js
// next.config.js
const path = require('path');

module.exports = {
  webpack: (config, { isServer }) => {
    if (isServer) {
      config.entry = async () => {
        const entries = await config.entry();
        entries['main.js'] = [
          path.resolve(__dirname, 'src/shims/windowShim.js'),
          ...entries['main.js'],
        ];
        return entries;
      };
    }
    return config;
  },
};

This approach should be a last resort; the guard or dynamic import is cleaner.

Why this happens (and how to avoid it next time)#

Next.js treats every page as a hybrid of server‑side rendering (SSR) and client‑side hydration. Anything that runs at the top level of a module is evaluated on the server first. Because window, document, and navigator are browser‑only globals, they must be accessed after the component mounts. To keep the problem from resurfacing:

  • Add an ESLint rule (no-restricted-globals) that flags direct window usage outside of a typeof guard.

  • Write a unit test that renders the component with next/jest in a Node environment and asserts that it does not throw.

  • Create a shared helper (src/lib/isBrowser.ts) that centralizes the guard:

    ts
    // src/lib/isBrowser.ts
    export const isBrowser = typeof window !== 'undefined';
    

    Then use if (isBrowser) { … } throughout the codebase.

By making the guard a first‑class citizen, you reduce the cognitive load and avoid accidental server crashes when adding new third‑party UI widgets.

Frequently Asked Questions

|

Have more questions? Contact us

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.