БлогУслугиКарьера
Обсудить проект
БлогУслугиКарьераОбсудить проект
Mini Apps

Telegram Mini App на Next.js: почему SDK падает на сервере

window is not defined при сборке Mini App на Next.js. Разбираем, почему use client не спасает, как развести SDK с серверным рендерингом и починить гидрацию.

Редакция Feature
Редакция Feature·
24 авг
·
13 мин
·
Telegram Mini App на Next.js: почему SDK падает на сервере

Локально всё работает. Запускаете сборку — и она падает:

ReferenceError: window is not defined

Разработчик добавляет "use client" в начало файла. Пересобирает. Падает точно так же. Добавляет ещё в пару файлов выше по дереву. Не помогает.

В этом месте обычно и происходит недоразумение, из-за которого можно потерять полдня. "use client" не означает «не выполнять на сервере». Он означает совсем другое, и пока это не уложилось, чинить бесполезно.

Разберём, что на самом деле происходит, и три рабочих способа развести Telegram SDK с серверным рендерингом. Всё проверено на @telegram-apps/sdk-react 3.3.9 и Next.js 16.

Главное недоразумение: «use client» не отключает сервер

Название директивы сбивает с толку. "use client" не выводит компонент из серверного рендеринга, а помечает границу, за которой код попадает в клиентский бандл и получает состояние, эффекты и обработчики.

Но клиентские компоненты всё равно предварительно отрисовываются на сервере. Next.js собирает по ним HTML, отдаёт его браузеру, а уже потом React оживляет разметку на клиенте. Это и есть гидрация.

Отсюда следствие, которое ломает Mini App: код клиентского компонента выполняется дважды: один раз в Node.js, где нет ни window, ни клиента Telegram, и второй раз в браузере. Первый прогон и роняет сборку.

Для сайтов со статическим экспортом, как этот проект (output: "export"), ничего не меняется: сервера в рантайме нет, но пререндер во время сборки есть, и падает именно он.

Что именно падает

Три вещи, по убыванию частоты.

Инициализация SDK. Вызов init() обращается к окружению, которого на сервере не существует.

Чтение параметров запуска. retrieveLaunchParams() бросает LaunchParamsRetrieveError, когда брать параметры неоткуда.

Прямые обращения к глобальным объектам. Любое window.Telegram или document на верхнем уровне модуля выполняется в момент импорта — до того, как отработает любая ваша проверка.

Последний пункт коварнее прочих. Проверка внутри компонента не спасает, если обращение стоит в теле модуля:

// ❌ выполнится при импорте, на сервере тоже
const tg = window.Telegram.WebApp;

export function useTelegram() {
  return tg;
}

Способ 1. Тонкая клиентская обёртка с отключённым SSR

Самый прямой путь: сказать Next.js не рендерить кусок дерева на сервере вообще.

Здесь есть подвох, на который натыкаются все. Опция ssr: false у next/dynamic не работает в серверных компонентах — App Router вернёт ошибку. Значит, нужна тонкая клиентская прослойка, единственная задача которой в том, чтобы объявить этот ssr: false:

// app/mini-app-shell.tsx
"use client";

import dynamic from "next/dynamic";

const MiniApp = dynamic(() => import("./mini-app"), {
  ssr: false,
  loading: () => <div>Загружаем…</div>,
});

export default function MiniAppShell() {
  return <MiniApp />;
}

Страница при этом остаётся серверным компонентом и подключает только обёртку:

// app/page.tsx
import MiniAppShell from "./mini-app-shell";

export default function Page() {
  return <MiniAppShell />;
}

Всё, что внутри ./mini-app, на сервер больше не попадает. Способ грубый, зато надёжный, и для приложения, которое всё равно живёт только внутри Telegram, он чаще всего и нужен.

Цена такого решения в отсутствии серверной разметки: пользователь увидит заглушку из loading, пока не приедет JavaScript.

Делаете Mini App на Next.js?

Поможем с архитектурой и доведём до релиза

Заказать Mini App

Способ 2. Инициализация в эффекте

Если выносить всё приложение не хочется, можно оставить дерево на месте, а инициализацию перенести в момент, который на сервере не наступает.

Эффекты выполняются только в браузере, и этого достаточно:

"use client";

import { useEffect, useState } from "react";
import { init, isTMA, miniAppReady } from "@telegram-apps/sdk-react";

export function TelegramProvider({ children }: { children: React.ReactNode }) {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    if (!isTMA()) return; // открыли не из Telegram

    const cleanup = init();
    miniAppReady();
    setReady(true);

    return cleanup; // init возвращает функцию очистки
  }, []);

  if (!ready) return <div>Загружаем…</div>;

  return <>{children}</>;
}

Два момента, которые часто упускают. init() возвращает функцию очистки, и её нужно вернуть из эффекта, иначе при повторном монтировании в строгом режиме обработчики накопятся. И проверка isTMA() обязана стоять до init(), иначе приложение упадёт при открытии обычной ссылкой в браузере.

Способ 3. Честный SSR через серверный снимок

Первые два способа жертвуют серверной разметкой. Если она нужна, например у Mini App есть публичная витрина, которую хочется отдавать сразу, то SDK умеет работать корректно.

Хук useSignal принимает вторым аргументом функцию, возвращающую значение для сервера:

"use client";

import { useSignal, backButton } from "@telegram-apps/sdk-react";

export function BackButtonHint() {
  // На сервере вернётся false, на клиенте реальное состояние сигнала
  const isVisible = useSignal(backButton.isVisible, () => false);

  return (
    <span>
      {isVisible ? "Кнопка назад активна" : "Навигация внутри приложения"}
    </span>
  );
}

Смысл второго аргумента именно в гидрации. React сверяет разметку, собранную на сервере, с тем, что получилось в браузере. Если сервер отрисовал одно, а клиент сразу же другое, вы получите предупреждение о расхождении, а в неудачных случаях React выбросит серверную разметку и перерисует поддерево целиком.

Серверный снимок делает первый клиентский рендер идентичным серверному. Реальное значение приезжает следующим рендером, уже после гидрации, и расхождения не возникает.

Для условий вне сигналов у пакета есть флаг isSSR:

import { isSSR } from "@telegram-apps/sdk-react";

if (!isSSR) {
  // безопасно трогать браузерное окружение
}

Ловушка, о которой узнают на проде: iframe

Этот пункт не про сборку и потому всплывает позже всех, уже после успешного деплоя. На телефоне приложение работает, а в веб-версии Telegram белый экран.

Причина в том, что веб-клиент Telegram открывает Mini App внутри iframe. Многие сборки Next.js по умолчанию или через конфигурацию отдают заголовок X-Frame-Options: DENY, а некоторые хостинги добавляют его сами. Браузер честно отказывается встраивать страницу, и пользователь видит пустоту.

Проверяется одной командой:

curl -sI https://ваш-домен.ru | grep -i "x-frame-options\|content-security-policy"

Если X-Frame-Options присутствует, его нужно убрать, а ограничение выразить через frame-ancestors в Content-Security-Policy, разрешив домены Telegram. Заголовок X-Frame-Options не умеет списка источников, поэтому заменить его на что-то избирательное нельзя, только снять.

Гидрация: три источника расхождений

Помимо сигналов SDK, разметка расходится ещё по трём причинам, и все три встречаются в Mini App постоянно.

Тема пользователя. Цвета приезжают из клиента Telegram, на сервере их нет. Не подставляйте их в разметку напрямую — привяжите к CSS-переменным через bindThemeParamsCssVars(), тогда сервер отдаёт нейтральный HTML, а цвета применяются стилями.

Размеры экрана. Высота из viewport на сервере неизвестна. Тот же приём: bindViewportCssVars() и вёрстка от переменных вместо 100vh.

Данные пользователя. Имя и аватар из initData на сервере недоступны. Отрисуйте на первом рендере плейсхолдер — тот же, что вернёт сервер.

Общее правило: всё, что известно только внутри Telegram, не должно влиять на первый рендер.

Обсудим ваш проект?

Оставьте контакты — перезвоним и обсудим задачу

Чек-лист перед деплоем

  • Понятно, что "use client" не отключает серверный пререндер
  • Нет обращений к window и document на верхнем уровне модулей
  • ssr: false объявлен внутри клиентского компонента, а не серверного
  • isTMA() проверяется до init()
  • Функция очистки из init() возвращается из эффекта
  • У useSignal задан серверный снимок там, где значение влияет на разметку
  • Цвета и высота берутся из CSS-переменных, а не из 100vh и не из темы напрямую
  • X-Frame-Options не отдаётся, ограничения выражены через frame-ancestors
  • Приложение проверено и на телефоне, и в веб-версии Telegram

Заключение

Почти все проблемы Mini App на Next.js сводятся к одной мысли: фреймворк по умолчанию считает, что страницу можно отрисовать без браузера, а Telegram SDK устроен ровно наоборот — он не существует вне клиента Telegram.

Дальше остаётся выбрать, где провести границу. Отключить серверный рендеринг целиком проще всего, и это подходит большинству приложений. Оставить его и аккуратно подсунуть серверные снимки дороже, но окупается, если у приложения есть публичная часть.

Ошибкой будет только третий вариант: оставить всё как есть и расставлять проверки на window по мере падений.

Читайте также

  • @telegram-apps/sdk-react: белый экран и ошибки инициализации Mini App
  • Создаём Telegram Mini App на React
  • Server Components и Client Components: где проходит граница

Обсудим ваш проект?

Оставьте контакты — перезвоним и обсудим задачу

Содержание
  • Главное недоразумение: «use client» не отключает сервер
  • Что именно падает
  • Способ 1. Тонкая клиентская обёртка с отключённым SSR
  • Способ 2. Инициализация в эффекте
  • Способ 3. Честный SSR через серверный снимок
  • Ловушка, о которой узнают на проде: iframe
  • Гидрация: три источника расхождений
  • Чек-лист перед деплоем
  • Заключение
Поделиться:

Похожие статьи

@telegram-apps/sdk-react: белый экран и ошибки инициализации Mini App
Mini Apps

@telegram-apps/sdk-react: белый экран и ошибки инициализации Mini App

12 мин
Вы пишете Telegram Mini App неправильно. Вот доказательство
Mini Apps

Вы пишете Telegram Mini App неправильно. Вот доказательство

12 мин
Как создать Telegram Mini App: полная инструкция для разработчика
Mini Apps

Как создать Telegram Mini App: полная инструкция для разработчика

14 мин
Feature IT

Feature IT — платформа по обучению программированию и разработке цифровых продуктов. Мы создаём современные веб-решения для бизнеса и обучаем этому других!

Политика конфиденциальностиПользовательское соглашение

О компании

  • Блог
  • Карьера

Услуги разработки

  • Разработка сайтов под ключ
  • Веб-приложения на React/Next.js
  • Telegram-боты для бизнеса
  • Mini Apps (Telegram, VK)
  • SEO-оптимизированные сайты
  • Автоматизация бизнес-процессов
  • Поддержка и развитие IT-продуктов

Обучение

  • Курс Python с нуля
  • Алгоритмы и структуры данных
  • Паттерны проектирования
  • Подготовка к собеседованиям в IT
  • Практика на реальных проектах

Инструменты

  • Генератор UTM-меток
  • Счётчик символов