Странствующий техник
Подкинуть работёнку
Назад к статьям
DEV25 июня 2026·4 MIN

window.ym is not a function в Next.js App Router: рабочее решение через Ya.Metrika2

Яндекс.Метрика через tag.js в Next.js App Router даёт window.ym undefined. Разбор причины и рабочее решение через Ya.Metrika2 instance API с отслеживанием SPA-переходов.

Подключил Яндекс.Метрику к лендингу на Next.js App Router через динамическую загрузку tag.js — счётчик инициализируется, скрипт грузится, а window.ym в консоли остаётся undefined. Причина в том, что tag.js без сниппета-очереди не создаёт window.ym вообще, а регистрирует только конструктор window.Ya.Metrika2. Ниже — рабочее решение через instance API вместо функции ym().

Час потерял на, казалось бы, очевидную вещь. Делюсь разбором, чтобы вы не теряли своё время на то же самое в App Router.

Проблема: tag.js не создаёт window.ym

Официальная документация Метрики предлагает вставить кусок кода — сниппет с очередью. Он создаёт window.ym как функцию-очередь, складывает вызовы до загрузки скрипта, потом исполняет. Стандартная схема.

Но в Next.js App Router я загружал скрипт динамически:

const script = document.createElement("script");
script.src = "https://mc.yandex.ru/metrika/tag.js";
script.async = true;
document.head.appendChild(script);
 
script.onload = () => {
  window.ym(107042392, "init", { clickmap: true });
  // TypeError: window.ym is not a function
};

tag.js загружается напрямую — без сниппета с очередью. Без очереди window.ym никто не создаёт. Скрипт просто регистрирует window.Ya.Metrika2 — конструктор счётчика. И всё.

Решение: Ya.Metrika2 instance API

Вместо ym() — создаём экземпляр счётчика напрямую и сохраняем в ref:

const counterRef = useRef<YaMetrikaCounter | null>(null);
 
script.onload = () => {
  counterRef.current = new window.Ya.Metrika2({
    id: 107042392,
    clickmap: true,
    trackLinks: true,
    accurateTrackBounce: true,
  });
 
  // первый hit — текущая страница
  counterRef.current.hit(window.location.pathname);
};

Для отслеживания переходов между страницами (App Router меняет URL без перезагрузки):

useEffect(() => {
  if (!counterRef.current) return;
 
  const currentPath = searchParams?.toString()
    ? `${pathname}?${searchParams}`
    : pathname;
 
  if (currentPath !== lastPathRef.current) {
    lastPathRef.current = currentPath;
    counterRef.current.hit(currentPath);
  }
}, [pathname, searchParams]);

Без .hit() при смене маршрута Метрика видит только первую страницу визита — остальное считается одним сеансом. Причина в том, что App Router меняет URL без полной перезагрузки страницы — та же особенность, которая ломает индексацию контента ботами Google, если положиться на клиентский роутинг. Подробнее — в посте про SEO-проблемы клиентского роутинга в App Router.

Типы

window.Ya нигде не задекларирован — TypeScript будет ругаться. Добавляю интерфейсы:

interface YaMetrikaCounter {
  hit: (url: string, options?: { referer?: string }) => void;
}
 
interface Ya {
  Metrika2: new (config: {
    id: number;
    clickmap?: boolean;
    trackLinks?: boolean;
    accurateTrackBounce?: boolean;
    webvisor?: boolean;
  }) => YaMetrikaCounter;
}
 
declare global {
  interface Window {
    Ya?: Ya;
  }
}

Pro Tips: Как получить максимум

  • Проверяй typeof window.Ya?.Metrika2 перед new — если скрипт заблокирован AdBlock, конструктора не будет, и без проверки получишь неперехваченный TypeError
  • Храни инстанс в ref, не в state — ре-рендер не нужен, нужна стабильная ссылка между эффектами
  • Первый .hit() вызывай сразу после init — иначе первая страница визита не фиксируется

Common Pitfalls: Где люди ошибаются

  • Копируют сниппет с window.ym в скрипт-тег — это работает в обычном HTML, но в App Router скрипт выполняется не в том месте и порядок гарантий нет
  • Не подписываются на смену маршрута — в App Router нет перезагрузки страницы, без useEffect на pathname Метрика видит только первый визит
  • Инициализируют счётчик без isInitializedRef — в React 18 Strict Mode эффекты запускаются дважды, получаешь два счётчика и задвоенные хиты

Идентификация

Денис Заплахов

Исследую Пустоши рутинных задач: настраиваю AI-инструменты, создаю скиллы и тестирую нейросети на реальных кейсах.

Досье в Pip-Boy 3000
© 2026 79z.ru · Странствующий техник