БлогУслугиКарьера
Обсудить проект
БлогУслугиКарьераОбсудить проект
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
  • Гидрация: три источника расхождений
  • Чек-лист перед деплоем
  • Заключение
Поделиться:

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

Корзина Mini App: как покупатель теряет выбранный размер
Mini Apps

Корзина Mini App: как покупатель теряет выбранный размер

9 мин
Повторный заказ в Mini App: что оставить на 1 экране
Mini Apps

Повторный заказ в Mini App: что оставить на 1 экране

8 мин
Telegram Mini App: 7 проверок перед запуском в 2026 году
Mini Apps

Telegram Mini App: 7 проверок перед запуском в 2026 году

8 мин
Feature IT

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

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

О компании

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

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

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

Обучение

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

Инструменты

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