search
next star Featured

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.

person By Gautam Sharma
calendar_today January 8, 2026
schedule 15 min read
Next.js document is not defined SSR Client-Side Browser API React JavaScript

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:

  1. Direct access to document or window in component render methods
  2. Using browser APIs in useEffect without proper checks
  3. Importing libraries that access browser APIs immediately
  4. Using DOM manipulation libraries without SSR compatibility
  5. 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.

Gautam Sharma

About Gautam Sharma

Full-stack developer and tech blogger sharing coding tutorials and best practices

Related Articles

next

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.

January 8, 2026
next

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.

January 8, 2026
next

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.

January 8, 2026