No articles found
Try different keywords or browse our categories
Fix: document is not defined in Next.js Error - Complete Client-Side Guide
Complete guide to fix 'document is not defined' error in Next.js applications. Learn how to handle browser APIs safely in server-side rendering environments.
The ‘document is not defined’ error is a common issue in Next.js applications that occurs when trying to access browser-specific APIs like document, window, or localStorage during server-side rendering. This error happens because these APIs are only available in the browser environment, not on the server. Understanding and resolving this error is crucial for building robust Next.js applications that work seamlessly in both server and client environments.
Understanding the Problem
Next.js uses server-side rendering (SSR) by default, which means components are initially rendered on the server before being sent to the client. The server environment doesn’t have access to browser APIs like document, window, or navigator. When your code tries to access these APIs during server rendering, you get the “document is not defined” error.
Common Scenarios Where This Error Occurs:
- Direct access to
documentorwindowin component render methods - Using browser APIs in useEffect without proper checks
- Importing libraries that access browser APIs immediately
- Using DOM manipulation libraries without SSR compatibility
- Accessing localStorage or sessionStorage during initial render
Solution 1: Check for Window Object
The most common approach is to check if the window object exists before accessing browser APIs.
❌ Without Proper Checks:
// components/BrowserComponent.jsx - ❌ Error-prone
import { useState } from 'react';
const BrowserComponent = () => {
// ❌ This will cause "document is not defined" error
const [screenWidth, setScreenWidth] = useState(document.documentElement.clientWidth);
return <div>Screen width: {screenWidth}px</div>;
};
export default BrowserComponent;
✅ With Window Checks:
components/BrowserComponent.jsx:
import { useState, useEffect } from 'react';
const BrowserComponent = () => {
const [screenWidth, setScreenWidth] = useState(0);
const [isClient, setIsClient] = useState(false);
useEffect(() => {
// ✅ Check if window exists before accessing browser APIs
if (typeof window !== 'undefined') {
setIsClient(true);
setScreenWidth(window.innerWidth);
const handleResize = () => {
setScreenWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// Cleanup event listener
return () => {
window.removeEventListener('resize', handleResize);
};
}
}, []);
return (
<div>
{isClient ? (
<p>Screen width: {screenWidth}px</p>
) : (
<p>Detecting screen size...</p>
)}
</div>
);
};
export default BrowserComponent;
components/DOMManipulationComponent.jsx:
import { useEffect, useRef } from 'react';
const DOMManipulationComponent = () => {
const elementRef = useRef(null);
useEffect(() => {
// ✅ Safe DOM manipulation after component mounts
if (typeof window !== 'undefined' && elementRef.current) {
// Access DOM element safely
elementRef.current.style.backgroundColor = '#f0f0f0';
// Access document safely
const title = document.title;
console.log('Page title:', title);
}
}, []);
return (
<div ref={elementRef}>
<h2>DOM Manipulation Example</h2>
<p>This component safely manipulates the DOM after mounting.</p>
</div>
);
};
export default DOMManipulationComponent;
Solution 2: Using Dynamic Imports with SSR Disabled
Use dynamic imports with { ssr: false } to completely skip server-side rendering for components that require browser APIs.
components/DynamicBrowserComponent.jsx:
import { useState, useEffect } from 'react';
const DynamicBrowserComponent = () => {
const [browserInfo, setBrowserInfo] = useState({
userAgent: '',
platform: '',
screenWidth: 0,
screenHeight: 0
});
useEffect(() => {
if (typeof window !== 'undefined') {
setBrowserInfo({
userAgent: navigator.userAgent,
platform: navigator.platform,
screenWidth: screen.width,
screenHeight: screen.height
});
}
}, []);
return (
<div className="browser-info">
<h3>Browser Information</h3>
<p><strong>User Agent:</strong> {browserInfo.userAgent}</p>
<p><strong>Platform:</strong> {browserInfo.platform}</p>
<p><strong>Screen Resolution:</strong> {browserInfo.screenWidth} x {browserInfo.screenHeight}</p>
</div>
);
};
export default DynamicBrowserComponent;
pages/example.jsx:
import { useState } from 'react';
import dynamic from 'next/dynamic';
// ✅ Dynamically import component with SSR disabled
const DynamicBrowserComponent = dynamic(
() => import('../components/DynamicBrowserComponent'),
{ ssr: false } // ✅ Skip server-side rendering
);
const ExamplePage = () => {
const [showComponent, setShowComponent] = useState(false);
return (
<div className="container">
<h1>Dynamic Import Example</h1>
<button onClick={() => setShowComponent(!showComponent)}>
{showComponent ? 'Hide' : 'Show'} Browser Info
</button>
{showComponent && <DynamicBrowserComponent />}
<div className="content">
<p>This content renders normally on the server.</p>
</div>
</div>
);
};
export default ExamplePage;
Solution 3: Custom Hook for Browser Detection
Create a custom hook to handle browser detection consistently across your application.
hooks/useIsomorphicLayoutEffect.js:
import { useEffect, useLayoutEffect } from 'react';
// ✅ Custom hook that uses layout effect on client, effect on server
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
hooks/useBrowser.js:
import { useState, useEffect } from 'react';
// ✅ Custom hook to detect if we're running in a browser
const useBrowser = () => {
const [isBrowser, setIsBrowser] = useState(false);
useEffect(() => {
setIsBrowser(typeof window !== 'undefined');
}, []);
return isBrowser;
};
export default useBrowser;
components/HookExample.jsx:
import useBrowser from '../hooks/useBrowser';
import { useState, useEffect } from 'react';
const HookExample = () => {
const isBrowser = useBrowser();
const [localStorageData, setLocalStorageData] = useState('');
useEffect(() => {
if (isBrowser) {
// ✅ Safe to access localStorage only in browser
const data = localStorage.getItem('myData');
setLocalStorageData(data || 'No data found');
}
}, [isBrowser]);
return (
<div>
<h2>Hook Example</h2>
{isBrowser ? (
<div>
<p>Local Storage Data: {localStorageData}</p>
<button onClick={() => localStorage.setItem('myData', 'Hello World')}>
Save to Local Storage
</button>
</div>
) : (
<p>Loading browser-specific content...</p>
)}
</div>
);
};
export default HookExample;
Solution 4: Environment-Specific Code
Use environment checks to conditionally execute browser-specific code.
utils/browserUtils.js:
// ✅ Utility functions that safely handle browser APIs
export const isBrowser = () => typeof window !== 'undefined';
export const getDocument = () => {
if (isBrowser()) {
return document;
}
return null;
};
export const getWindow = () => {
if (isBrowser()) {
return window;
}
return null;
};
export const getLocalStorage = () => {
if (isBrowser() && window.localStorage) {
return window.localStorage;
}
return null;
};
export const getSessionStorage = () => {
if (isBrowser() && window.sessionStorage) {
return window.sessionStorage;
}
return null;
};
// ✅ Safe DOM manipulation function
export const safeDocumentQuery = (selector) => {
const doc = getDocument();
if (doc) {
return doc.querySelector(selector);
}
return null;
};
// ✅ Safe window property access
export const getWindowDimensions = () => {
const win = getWindow();
if (win) {
return {
width: win.innerWidth,
height: win.innerHeight
};
}
return { width: 0, height: 0 };
};
// ✅ Safe localStorage operations
export const safeLocalStorage = {
getItem: (key) => {
const storage = getLocalStorage();
if (storage) {
try {
return storage.getItem(key);
} catch (error) {
console.error('Error accessing localStorage:', error);
return null;
}
}
return null;
},
setItem: (key, value) => {
const storage = getLocalStorage();
if (storage) {
try {
storage.setItem(key, value);
} catch (error) {
console.error('Error setting localStorage:', error);
}
}
},
removeItem: (key) => {
const storage = getLocalStorage();
if (storage) {
try {
storage.removeItem(key);
} catch (error) {
console.error('Error removing from localStorage:', error);
}
}
}
};
components/UtilityExample.jsx:
import { useState, useEffect } from 'react';
import { isBrowser, getWindowDimensions, safeLocalStorage } from '../utils/browserUtils';
const UtilityExample = () => {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [savedValue, setSavedValue] = useState('');
const [inputValue, setInputValue] = useState('');
useEffect(() => {
if (isBrowser()) {
// ✅ Safe access to browser APIs using utilities
setDimensions(getWindowDimensions());
// ✅ Safe localStorage access
const storedValue = safeLocalStorage.getItem('exampleValue');
if (storedValue) {
setSavedValue(storedValue);
setInputValue(storedValue);
}
const handleResize = () => {
setDimensions(getWindowDimensions());
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}
}, []);
const handleSave = () => {
if (isBrowser()) {
safeLocalStorage.setItem('exampleValue', inputValue);
setSavedValue(inputValue);
}
};
return (
<div>
<h2>Utility Functions Example</h2>
<p>Window Dimensions: {dimensions.width} x {dimensions.height}</p>
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Enter value to save"
/>
<button onClick={handleSave}>Save to Local Storage</button>
</div>
<p>Saved Value: {savedValue || 'No value saved'}</p>
</div>
);
};
export default UtilityExample;
Solution 5: Third-Party Library Integration
Handle third-party libraries that access browser APIs.
components/ChartComponent.jsx:
import { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
// ✅ Dynamically import chart library with SSR disabled
const Chart = dynamic(
() => import('react-chartjs-2').then((mod) => mod.Bar),
{
ssr: false,
loading: () => <p>Loading chart...</p>
}
);
const ChartComponent = () => {
const [chartData, setChartData] = useState(null);
useEffect(() => {
if (typeof window !== 'undefined') {
// ✅ Prepare chart data only on client
setChartData({
labels: ['January', 'February', 'March', 'April', 'May'],
datasets: [
{
label: 'Sales',
data: [12, 19, 3, 5, 2],
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1,
},
],
});
}
}, []);
const chartOptions = {
responsive: true,
plugins: {
legend: {
position: 'top',
},
},
};
return (
<div>
<h2>Sales Chart</h2>
{chartData && <Chart data={chartData} options={chartOptions} />}
</div>
);
};
export default ChartComponent;
Working Code Examples
Complete Next.js Page with Proper Browser API Handling:
// pages/browser-demo.jsx
import { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
import useBrowser from '../hooks/useBrowser';
import { isBrowser, safeLocalStorage } from '../utils/browserUtils';
// ✅ Dynamically import components that require browser APIs
const DynamicChartComponent = dynamic(
() => import('../components/ChartComponent'),
{ ssr: false }
);
const BrowserDemoPage = () => {
const isBrowserEnv = useBrowser();
const [browserFeatures, setBrowserFeatures] = useState({});
const [localStorageSupported, setLocalStorageSupported] = useState(false);
useEffect(() => {
if (isBrowserEnv) {
// ✅ Gather browser information safely
setBrowserFeatures({
userAgent: navigator.userAgent,
language: navigator.language,
online: navigator.onLine,
cookiesEnabled: navigator.cookieEnabled,
platform: navigator.platform,
hardwareConcurrency: navigator.hardwareConcurrency || 'Unknown',
deviceMemory: navigator.deviceMemory || 'Unknown'
});
// ✅ Check localStorage support
try {
const test = 'test';
localStorage.setItem(test, test);
localStorage.removeItem(test);
setLocalStorageSupported(true);
} catch (e) {
setLocalStorageSupported(false);
}
}
}, [isBrowserEnv]);
const [counter, setCounter] = useState(0);
const incrementCounter = () => {
if (isBrowserEnv) {
const newCounter = counter + 1;
setCounter(newCounter);
safeLocalStorage.setItem('counter', newCounter.toString());
}
};
const loadCounter = () => {
if (isBrowserEnv) {
const savedCounter = safeLocalStorage.getItem('counter');
if (savedCounter) {
setCounter(parseInt(savedCounter, 10));
}
}
};
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">Browser API Demo</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Browser Information</h2>
{isBrowserEnv ? (
<div className="space-y-2">
<p><strong>User Agent:</strong> {browserFeatures.userAgent}</p>
<p><strong>Language:</strong> {browserFeatures.language}</p>
<p><strong>Online:</strong> {browserFeatures.online ? 'Yes' : 'No'}</p>
<p><strong>Cookies Enabled:</strong> {browserFeatures.cookiesEnabled ? 'Yes' : 'No'}</p>
<p><strong>Platform:</strong> {browserFeatures.platform}</p>
<p><strong>Hardware Concurrency:</strong> {browserFeatures.hardwareConcurrency}</p>
<p><strong>Device Memory:</strong> {browserFeatures.deviceMemory}</p>
</div>
) : (
<p>Loading browser information...</p>
)}
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Local Storage Demo</h2>
<div className="space-y-4">
<p>Local Storage Supported: {localStorageSupported ? 'Yes' : 'No'}</p>
<div>
<p>Counter: {counter}</p>
<button
onClick={incrementCounter}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-2"
>
Increment
</button>
<button
onClick={loadCounter}
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
>
Load Saved
</button>
</div>
</div>
</div>
</div>
<div className="mt-8 bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Chart Component</h2>
<DynamicChartComponent />
</div>
</div>
);
};
export default BrowserDemoPage;
Custom App Component for Global Browser Checks:
// pages/_app.js
import '../styles/globals.css';
import { useState, useEffect } from 'react';
function MyApp({ Component, pageProps }) {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
// ✅ Mark as mounted after initial render
setIsMounted(true);
}, []);
// ✅ Prevent rendering of browser-dependent components until mounted
if (!isMounted) {
return (
<div className="flex justify-center items-center h-screen">
<p>Loading...</p>
</div>
);
}
return <Component {...pageProps} />;
}
export default MyApp;
Best Practices for Browser API Handling
1. Always Check for Window Object
// ✅ Good practice
if (typeof window !== 'undefined') {
// Browser-specific code here
const width = window.innerWidth;
}
2. Use Dynamic Imports for Browser-Only Components
// ✅ Good practice
const BrowserOnlyComponent = dynamic(() => import('../components/BrowserComponent'), {
ssr: false
});
3. Implement Proper Error Boundaries
// components/ErrorBoundary.jsx
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
4. Use Conditional Rendering
// ✅ Good practice
const MyComponent = () => {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return (
<div>
{isClient && (
<div>
{/* Browser-specific content */}
</div>
)}
</div>
);
};
5. Handle Storage Operations Safely
// ✅ Good practice
const safeStorageOperation = (key, value) => {
if (typeof window !== 'undefined' && window.localStorage) {
try {
localStorage.setItem(key, value);
} catch (error) {
console.error('Storage operation failed:', error);
}
}
};
Debugging Steps
Step 1: Identify the Problematic Code
# Look for direct access to browser APIs
grep -r "document\|window\|localStorage" src/
Step 2: Check Component Mounting
// Add logging to identify when components mount
useEffect(() => {
console.log('Component mounted on client');
}, []);
Step 3: Verify Dynamic Imports
// Ensure ssr: false is set for browser-only components
const Component = dynamic(() => import('./Component'), { ssr: false });
Step 4: Test Server-Side Rendering
# Build and test SSR
npm run build
npm start
Common Mistakes to Avoid
1. Direct Browser API Access in Render
// ❌ Don't do this
const Component = () => {
const width = window.innerWidth; // ❌ Error during SSR
return <div>{width}</div>;
};
2. Forgetting to Check for Window
// ❌ Don't do this
useEffect(() => {
document.title = 'My Page'; // ❌ May cause error
}, []);
3. Not Handling Dynamic Imports Properly
// ❌ Don't do this
import Chart from 'react-chartjs-2'; // ❌ May cause errors
4. Ignoring Error Boundaries
// ❌ Don't skip error handling
// Always wrap browser-dependent code with error boundaries
Performance Considerations
1. Lazy Load Browser-Dependent Components
// ✅ Good for performance
const HeavyBrowserComponent = dynamic(
() => import('../components/HeavyComponent'),
{
ssr: false,
loading: () => <SkeletonLoader />
}
);
2. Optimize Re-renders
// ✅ Prevent unnecessary re-renders
const [isClient, setIsClient] = useState(false);
useEffect(() => {
if (typeof window !== 'undefined') {
setIsClient(true);
}
}, []); // ✅ Empty dependency array
3. Memoize Expensive Calculations
// ✅ Use useMemo for expensive browser operations
const expensiveValue = useMemo(() => {
if (typeof window !== 'undefined') {
return performExpensiveCalculation();
}
return null;
}, []);
Security Considerations
1. Validate Browser API Inputs
// ✅ Validate inputs before using browser APIs
const safeLocalStorageSet = (key, value) => {
if (typeof window !== 'undefined' &&
window.localStorage &&
typeof key === 'string' &&
key.length < 1000) {
try {
localStorage.setItem(key, value);
} catch (error) {
// Handle error appropriately
}
}
};
2. Sanitize DOM Manipulation
// ✅ Sanitize content before DOM manipulation
const safeInnerHTML = (content) => {
if (typeof window !== 'undefined') {
const div = document.createElement('div');
div.textContent = content; // ✅ Use textContent to prevent XSS
return div.innerHTML;
}
return '';
};
Testing Browser API Code
1. Test SSR Compatibility
// test/ssr.test.js
describe('SSR Compatibility', () => {
it('should not throw errors during server-side rendering', () => {
// Test that components don't access browser APIs during SSR
expect(() => {
require('../components/MyComponent');
}).not.toThrow();
});
});
2. Test Client-Side Functionality
// test/client.test.js
describe('Client-Side Functionality', () => {
it('should work properly in browser environment', () => {
// Mock browser APIs for testing
Object.defineProperty(window, 'localStorage', {
value: {
getItem: jest.fn(() => 'test'),
setItem: jest.fn(),
removeItem: jest.fn(),
},
writable: true,
});
// Test component functionality
});
});
Alternative Solutions
1. Use Next.js Built-in Features
// ✅ Use Next.js router for client-side navigation
import { useRouter } from 'next/router';
const Component = () => {
const router = useRouter();
useEffect(() => {
// ✅ Router is safe to use after mount
console.log(router.pathname);
}, [router.pathname]);
};
2. Leverage Next.js Data Fetching
// ✅ Use getServerSideProps for server-side data
export async function getServerSideProps() {
// Server-side code here
return { props: { /* data */ } };
}
3. Use Third-Party Libraries
// ✅ Use libraries designed for Next.js
import { useWindowWidth } from '@react-hook/window-size';
const Component = () => {
const windowWidth = useWindowWidth(); // ✅ Handles SSR automatically
return <div>Width: {windowWidth}</div>;
};
Migration Checklist
- Identify all direct browser API accesses
- Implement window existence checks
- Use dynamic imports for browser-only components
- Create custom hooks for browser detection
- Implement error boundaries
- Test SSR compatibility
- Verify client-side functionality
- Add proper loading states
- Update documentation
- Run comprehensive tests
Conclusion
The ‘document is not defined’ error in Next.js occurs when browser-specific APIs are accessed during server-side rendering. By following the solutions provided in this guide—implementing proper browser detection, using dynamic imports, creating custom hooks, and following best practices—you can effectively prevent and resolve this error in your Next.js applications.
The key is to understand the difference between server and client environments, implement proper safeguards when accessing browser APIs, use Next.js features appropriately, and maintain clean, well-organized code. With proper browser API handling, your Next.js applications will work seamlessly in both server and client environments and avoid common SSR-related errors.
Remember to test your changes thoroughly, follow Next.js best practices for SSR, implement proper error handling, and regularly review your browser API usage to ensure your applications maintain the best possible architecture and avoid common “document is not defined” errors.
Related Articles
Fix: window is not defined in Next.js Project Error
Learn how to fix the 'window is not defined' error in Next.js applications. This comprehensive guide covers client-side only code, dynamic imports, and proper browser API usage.
Fix: Hydration failed because the initial UI does not match error in Next.js - Complete Hydration Guide
Complete guide to fix 'Hydration failed because the initial UI does not match' error in Next.js applications. Learn how to handle client-server rendering mismatches and implement proper hydration strategies.
Fix: Text content does not match server-rendered HTML error in Next.js - Quick Solutions
Quick guide to fix 'Text content does not match server-rendered HTML' errors in Next.js. Essential fixes with minimal code examples.