Каждый, кто работал с 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 выявили несколько критических моментов:
-
Проблемы гидратации: если сервер и клиент генерируют разный HTML, React выбросит ошибку. Всегда используйте паттерн с проверкой монтирования компонента.
-
Контекст-провайдеры: как отмечают на Reddit, если библиотека предоставляет провайдер с обращением к window, придется загружать весь контекст динамически.
-
Различия между 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 приложений.