SSR и Pikaday

SSR-совместимость Pikaday напрямую связана с тем, что библиотека изначально проектировалась для работы в браузере и опирается на глобальные объекты window и document. При серверном рендеринге эти сущности отсутствуют, поэтому любая попытка инициализации календаря на этапе выполнения кода на сервере приводит к падению приложения или к некорректному результату гидратации.

Ключевая проблема SSR в контексте Pikaday заключается в том, что библиотека не разделяет этапы «создание экземпляра» и «подключение к DOM» достаточно строго. Конструктор new Pikaday() ожидает наличие DOM-элемента и браузерного окружения, а значит должен выполняться только на клиенте.

При серверном рендеринге код выполняется в среде Node.js, где отсутствуют:

  • window
  • document
  • HTMLInputElement как реальный DOM-узел
  • layout-расчёты и события браузера

Типичная ошибка при неаккуратной интеграции:

import Pikaday from 'pikaday';

const picker = new Pikaday({
  field: document.getElementById('date') // ❌ document не существует на сервере
});

При выполнении на сервере такой код вызывает ReferenceError: document is not defined.

Даже если обёртка компонента скрывает доступ к DOM, сам факт импорта и немедленной инициализации библиотеки уже нарушает SSR-цепочку.

Разделение серверного и клиентского контекста

Корректная интеграция требует строгого разделения:

  • сервер: рендерит HTML-разметку без интерактивных зависимостей
  • клиент: инициализирует Pikaday после монтирования DOM

Основной принцип — любая работа с Pikaday должна происходить только после mount-фазы.

Проверка наличия браузерной среды

Самый простой защитный механизм:

const isBrowser = typeof window !== 'undefined';

Однако этого недостаточно, если импорт модуля уже триггерит обращение к document. Поэтому важнее использовать динамическую загрузку.

Динамический импорт Pikaday

В SSR-архитектурах предпочтительным способом является ленивый импорт:

let Pikaday;

if (typeof window !== 'undefined') {
  import('pikaday').then(module => {
    Pikaday = module.default;

    const picker = new Pikaday({
      field: document.getElementById('date')
    });
  });
}

Такой подход гарантирует, что код библиотеки вообще не попадает в серверный бандл.

В более современных сборках предпочтительнее await import внутри lifecycle-методов.

React и SSR: корректная интеграция

В React-экосистеме SSR (например, Next.js) жизненный цикл играет ключевую роль. Основное правило: инициализация должна происходить внутри useEffect, который не выполняется на сервере.

import { useEffect, useRef } from 'react';

export default function DatePicker() {
  const inputRef = useRef(null);

  useEffect(() => {
    let picker;

    import('pikaday').then(({ default: Pikaday }) => {
      picker = new Pikaday({
        field: inputRef.current
      });
    });

    return () => {
      if (picker) {
        picker.destroy();
      }
    };
  }, []);

  return <input ref={inputRef} type="text" />;
}

Ключевой момент: useEffect гарантирует отсутствие выполнения на сервере, что полностью решает проблему SSR-конфликта.

Next.js и отключение SSR для компонента

В Next.js часто применяется стратегия полного отключения SSR для конкретного компонента:

import dynamic from 'next/dynamic';

const DatePicker = dynamic(() => import('./DatePicker'), {
  ssr: false
});

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

Это наиболее «жёсткий» способ изоляции Pikaday. Он эффективен, но увеличивает клиентскую нагрузку, так как компонент становится полностью клиентским.

Vue и SSR: использование onMounted

Во Vue SSR (Nuxt или кастомные сборки) применяется lifecycle onMounted:

import { onMounted, ref } from 'vue';

export default {
  setup() {
    const input = ref(null);

    onMounted(async () => {
      const { default: Pikaday } = await import('pikaday');

      new Pikaday({
        field: input.value
      });
    });

    return { input };
  }
};

Vue гарантирует, что onMounted выполняется только на клиенте, что делает этот способ безопасным.

Angular SSR и платформенная проверка

В Angular Universal используется проверка платформы:

import { isPlatformBrowser } from '@angular/common';
import { Inject, PLATFORM_ID, AfterViewInit } from '@angular/core';

declare const Pikaday: any;

export class DateComponent implements AfterViewInit {
  constructor(@Inject(PLATFORM_ID) private platformId: object) {}

  ngAfterViewInit() {
    if (isPlatformBrowser(this.platformId)) {
      const input = document.getElementById('date');

      new Pikaday({
        field: input
      });
    }
  }
}

Здесь ключевую роль играет AfterViewInit, так как DOM гарантированно уже существует.

Гидратация и проблемы несоответствия DOM

SSR не ограничивается отсутствием window. Вторая проблема — hydration mismatch.

Pikaday может:

  • изменять DOM после загрузки
  • добавлять собственные контейнеры календаря
  • модифицировать input-элемент

Если сервер уже отрендерил HTML, а клиент сразу изменяет структуру до завершения гидратации, возникают расхождения.

Решение: отложенная инициализация

Лучший подход — инициализация только после завершения гидратации:

useEffect(() => {
  const timer = setTimeout(() => {
    import('pikaday').then(({ default: Pikaday }) => {
      new Pikaday({ field: inputRef.current });
    });
  }, 0);

  return () => clearTimeout(timer);
}, []);

Задержка позволяет завершить первичную синхронизацию React/Vue с DOM.

Изоляция DOM-манипуляций

Pikaday активно создаёт вспомогательные DOM-узлы календаря. В SSR-архитектуре важно обеспечить:

  • единый контейнер для календаря
  • отсутствие конфликтов с порталами (React portals, Vue Teleport)
  • предсказуемую очистку при unmount

Пример:

new Pikaday({
  field: inputRef.current,
  bound: true,
  container: document.body
});

Явное указание контейнера снижает риск расхождения DOM-структур.

Предрендеринг и статическая генерация

В SSG (Static Site Generation) ситуация проще, чем в SSR. Поскольку HTML генерируется один раз и не требует реального времени выполнения на сервере, Pikaday не участвует в генерации вообще.

Однако важно:

  • не выполнять инициализацию на этапе build
  • исключать зависимости через динамический импорт
  • использовать lazy hydration

Обработка локалей и дат в SSR

Дополнительная сложность — работа с датами:

  • сервер может использовать UTC
  • клиент — локальное время пользователя
  • Pikaday отображает локализованные строки

Это приводит к расхождению отображаемых значений.

Решение:

  • передавать даты в ISO-формате
  • избегать форматирования на сервере
  • делегировать форматирование клиенту
const initialDate = new Date().toISOString();

Полная изоляция через фабричный паттерн

Наиболее устойчивый подход — обёртка над Pikaday:

export async function createDatePicker(input) {
  if (typeof window === 'undefined') return null;

  const { default: Pikaday } = await import('pikaday');

  return new Pikaday({
    field: input
  });
}

Это позволяет централизовать всю SSR-логику и избежать дублирования проверок в компонентах.

Управление жизненным циклом

Важно учитывать разрушение экземпляра:

  • SSR не создаёт экземпляр
  • клиент создаёт его асинхронно
  • при переходах маршрутов экземпляр должен уничтожаться
picker.destroy();
picker = null;

Без этого в SPA-режиме возникают утечки DOM-событий и дублирование календарей.

Оптимизация загрузки

Pikaday достаточно лёгкая библиотека, но в SSR-архитектурах важно:

  • выносить её в отдельный chunk
  • загружать только при взаимодействии пользователя
  • избегать eager import

Пример ленивой стратегии:

input.addEventListener('focus', async () => {
  const { default: Pikaday } = await import('pikaday');

  new Pikaday({ field: input });
}, { once: true });

Такой подход уменьшает нагрузку на initial render и ускоряет TTI.

Поведение в условиях прогрессивного рендеринга

При streaming SSR (например, React 18) важно учитывать, что часть DOM может быть уже интерактивной, пока остальная страница ещё загружается. Pikaday в таких условиях должен:

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

Иначе возможны дубли календарей при повторной гидратации сегментов страницы.

Итоговая архитектурная модель интеграции

Устойчивое поведение Pikaday в SSR-среде достигается комбинацией принципов:

  • полное исключение доступа к window на сервере
  • динамический импорт библиотеки
  • инициализация только в клиентских lifecycle-хуках
  • контроль гидратации и DOM-расхождений
  • явное управление жизненным циклом экземпляра
  • ленивое подключение по событию пользователя