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.
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:
// 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:
- Guard the reference with
typeof window !== 'undefined'. - Move the code into a
useEffecthook so it only runs after the component mounts in the browser. - Load the library dynamically with
next/dynamicand 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.
// 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:
// 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: '© 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#
-
Create a new file
src/lib/LeafletMap.tsx(or whatever library you’re using) and paste the second snippet above. -
Replace the original import in
src/components/Map.tsxwith the dynamic import shown in the first snippet. -
Remove any top‑level
windowusage from the original component. If you still need a small check, wrap it:jsif (typeof window !== 'undefined') { // browser‑only code } -
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:
// 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:
npm run dev
You should see the usual Next.js startup log without any ReferenceError:
> 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:
// src/utils/browserOnly.ts
export const isMobile = () => window.innerWidth < 768;
In that case, the guard must be inside the utility:
// 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:
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:
// src/shims/windowShim.js
if (typeof window === 'undefined') {
global.window = {} as any;
}
Add this shim as the first import in next.config.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 directwindowusage outside of atypeofguard. -
Write a unit test that renders the component with
next/jestin 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.
Related#
Frequently Asked Questions
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Related Guides
NextJS Warning: Extra attributes from the server – Fix 2026
Seeing the warning "Extra attributes from the server: data‑new‑gr‑c‑s‑check‑loaded"? Learn why it appears and how to eliminate it in your Next.js + Supabase app.
Supabase Realtime: Guide to Building Live Applications
Master Supabase Realtime with this comprehensive guide. Learn Postgres Changes, Presence, Broadcast, and build real-time features like chat, notifications, and collaborative editing.
Supabase Storage: Guide to File Uploads and Management
Master Supabase Storage with this comprehensive guide. Learn file uploads, image optimization, CDN delivery, security policies, and advanced patterns for production applications.