Каждый, кто работал с Next.js, рано или поздно сталкивается с этой ошибкой. Вы написали код, проверили локально, все работает. Деплоите на Vercel или другой хостинг, и вдруг приложение падает с загадочным сообщением "ReferenceError: window is not defined". Почему так происходит и как это исправить раз и навсегда?

Два мира JavaScript: сервер против браузера

Next.js выполняет код в двух разных средах. На сервере, где работает Node.js, генерируется HTML для отправки пользователю. В браузере этот HTML оживает благодаря JavaScript и становится интерактивным. Проблема в том, что на сервере просто не существует объектов window, document, localStorage и других браузерных API.

Согласно информации из GeeksforGeeks и официальной документации Next.js, эта ошибка возникает именно потому, что фреймворк по умолчанию использует Server-Side Rendering (SSR) или Static Site Generation (SSG). Код сначала выполняется в Node.js окружении, где глобальных браузерных объектов попросту нет.

Интересный факт из сообщества Reddit: даже если вы используете директиву "use client" в App Router (Next.js 13+), это не гарантирует, что код не будет выполнен на сервере. Как отмечают разработчики на Stack Overflow, Client Components все равно проходят предварительный рендеринг на сервере для генерации начального HTML, а затем гидратируются на клиенте.

Проверенные способы решения проблемы

Метод первый: защитная проверка typeof window

Самый простой способ избежать ошибки - проверять наличие window перед его использованием:

if (typeof window !== 'undefined') {
  // Безопасно использовать window
  const width = window.innerWidth;
  console.log('Ширина экрана:', width);
}

Этот подход рекомендуют многие источники, включая GeeksforGeeks и DEV Community. Однако есть важный нюанс: не используйте эту проверку для условного рендеринга разного контента, иначе получите ошибку гидратации (hydration mismatch).

Метод второй: хук useEffect для клиентской логики

По данным Edureka и множества ответов на Stack Overflow, перенос логики в useEffect - наиболее React-правильный подход:

import { useEffect, useState } from 'react';

function WindowSizeComponent() {
  const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
  
  useEffect(() => {
    function updateSize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    }
    
    updateSize();
    window.addEventListener('resize', updateSize);
    
    return () => window.removeEventListener('resize', updateSize);
  }, []);
  
  return (
    <div>
      Размер окна: {windowSize.width} x {windowSize.height}
    </div>
  );
}

Метод третий: динамический импорт с отключением SSR

Это решение особенно популярно на Stack Overflow и в официальной документации Next.js. Когда целый компонент или библиотека несовместимы с серверным рендерингом:

import dynamic from 'next/dynamic';

const MapComponent = dynamic(
  () => import('../components/InteractiveMap'),
  { 
    ssr: false,
    loading: () => <div>Загрузка карты...</div>
  }
);

export default function LocationPage() {
  return (
    <div>
      <h1>Наше местоположение</h1>
      <MapComponent />
    </div>
  );
}

Как отмечают разработчики на Reddit, этот метод идеально подходит для библиотек типа react-leaflet, Chart.js, или любых компонентов с 3D-графикой.

Особенности Next.js 13-15: App Router и Server Components

С появлением App Router ситуация усложнилась. Теперь есть явное разделение на Server Components (по умолчанию) и Client Components (с директивой "use client").

Важное замечание из официальной документации Next.js: нельзя сочетать suspense: true и ssr: false в динамическом импорте. Фреймворк просто проигнорирует опцию ssr: false, что может привести к неожиданному выполнению кода на сервере.

Многие разработчики на DEV Community рекомендуют создавать специальный хук для определения среды выполнения:

'use client';
import { useEffect, useState } from 'react';

export function useIsClient() {
  const [isClient, setIsClient] = useState(false);
  
  useEffect(() => {
    setIsClient(true);
  }, []);
  
  return isClient;
}

// Использование
function MyComponent() {
  const isClient = useIsClient();
  
  if (!isClient) {
    return <div>Загрузка...</div>;
  }
  
  return <div>Ширина экрана: {window.innerWidth}</div>;
}

Работа со сторонними библиотеками

Согласно исследованиям сообщества на Reddit и Stack Overflow, большинство проблем возникает именно со сторонними библиотеками. Например, библиотеки для работы с картами (Mapbox, Leaflet), графиками (ApexCharts, Chart.js), редакторами текста (Quill, TipTap) часто обращаются к window сразу при импорте.

Решение от разработчиков на codestudy: оборачивайте проблемные библиотеки в отдельные компоненты-адаптеры:

// components/ChartWrapper.js
'use client';
import { useEffect, useState } from 'react';

export default function ChartWrapper({ data }) {
  const [Chart, setChart] = useState(null);
  
  useEffect(() => {
    import('react-chartjs-2').then(module => {
      setChart(() => module.Line);
    });
  }, []);
  
  if (!Chart) return <div>Загрузка графика...</div>;
  
  return <Chart data={data} />;
}

Оптимизация производительности и ленивая загрузка

Информация из DEV Community показывает, что динамическая загрузка библиотек по требованию может существенно улучшить производительность:

export default function PDFExportButton() {
  const [isGenerating, setIsGenerating] = useState(false);
  
  const handleExport = async () => {
    setIsGenerating(true);
    
    // Загружаем jsPDF только когда пользователь нажал кнопку
    const jsPDF = (await import('jspdf')).default;
    
    const doc = new jsPDF();
    doc.text('Отчет', 10, 10);
    doc.save('report.pdf');
    
    setIsGenerating(false);
  };
  
  return (
    <button onClick={handleExport} disabled={isGenerating}>
      {isGenerating ? 'Генерация...' : 'Скачать PDF'}
    </button>
  );
}

Подводные камни и как их избежать

Исследования на Sentry.io выявили несколько критических моментов:

  1. Проблемы гидратации: если сервер и клиент генерируют разный HTML, React выбросит ошибку. Всегда используйте паттерн с проверкой монтирования компонента.

  2. Контекст-провайдеры: как отмечают на Reddit, если библиотека предоставляет провайдер с обращением к window, придется загружать весь контекст динамически.

  3. Различия между dev и production: многие разработчики на DEV Community подчеркивают важность тестирования production-сборки командой next build && next start.

Сравнительная таблица решений

На основе анализа всех источников, включая официальную документацию и опыт сообщества:

Метод Когда использовать Влияние на SEO
typeof window проверка Мелкие фрагменты кода Минимальное
useEffect Логика после монтирования Нейтральное
dynamic с ssr: false Целые компоненты/библиотеки Может ухудшить
Ленивая загрузка Тяжелые библиотеки Улучшает производительность
use client директива App Router компоненты Зависит от реализации

Практический кейс: работа с localStorage

Частая проблема, описанная на Stack Overflow и GeeksforGeeks:

'use client';
import { useEffect, useState } from 'react';

function ThemeToggle() {
  // Начальное значение для SSR
  const [theme, setTheme] = useState('light');
  const [mounted, setMounted] = useState(false);
  
  useEffect(() => {
    setMounted(true);
    // Читаем сохраненную тему только на клиенте
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      setTheme(savedTheme);
    }
  }, []);
  
  const toggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light';
    setTheme(newTheme);
    if (mounted) {
      localStorage.setItem('theme', newTheme);
    }
  };
  
  // Избегаем мерцания при гидратации
  if (!mounted) {
    return <button disabled>Загрузка темы...</button>;
  }
  
  return (
    <button onClick={toggleTheme}>
      Текущая тема: {theme}
    </button>
  );
}

Выводы и рекомендации

Ошибка "window is not defined" в Next.js - это не баг, а архитектурная особенность фреймворка. Как показывает опыт тысяч разработчиков на Stack Overflow, Reddit и других платформах, понимание причин возникновения этой ошибки делает ее решение тривиальной задачей.

Ключевые рекомендации от сообщества и официальных источников:

  • Всегда помните о двойственной природе кода в Next.js

  • Используйте useEffect для браузерных API

  • Применяйте динамический импорт для проблемных библиотек

  • Тестируйте production-сборку перед деплоем

  • Создавайте переиспользуемые хуки для типовых задач

Современные версии Next.js (14-15) предоставляют все необходимые инструменты для элегантного решения этой проблемы. Выбор конкретного метода зависит от вашего случая: простая проверка typeof window подойдет для небольших фрагментов, useEffect - для хуков и состояния, а динамический импорт - для целых компонентов и библиотек.

Помните, что правильное разделение серверного и клиентского кода не только решает проблемы с window, но и улучшает производительность, SEO и общую архитектуру приложения. Каждый метод имеет свои преимущества и недостатки, но вместе они формируют мощный арсенал для создания надежных Next.js приложений.