Интеграция с React

Библиотека Pikaday была создана как независимый JavaScript-компонент и не содержит встроенной поддержки React. В отличие от современных React-библиотек, Pikaday напрямую работает с DOM-элементами через ссылки на поля ввода. Поэтому при интеграции требуется учитывать особенности жизненного цикла React-компонентов, управление состоянием и корректное уничтожение экземпляров календаря.

Основная идея интеграции заключается в том, что React отвечает за отображение интерфейса и управление состоянием, а Pikaday подключается к конкретному DOM-элементу после его создания.


Установка зависимостей

Установка выполняется через npm:

npm install pikaday

или через Yarn:

yarn add pikaday

После установки библиотека может импортироваться непосредственно в React-компонент:

import Pikaday from "pikaday";
import "pikaday/css/pikaday.css";

Подключение CSS-файла обеспечивает отображение стандартного оформления календаря.


Использование useRef для доступа к DOM-элементу

Поскольку Pikaday требует ссылку на реальное поле ввода, используется хук useRef.

Простейший пример:

import React, { useEffect, useRef } from "react";
import Pikaday from "pikaday";
import "pikaday/css/pikaday.css";

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

  useEffect(() => {
    const picker = new Pikaday({
      field: inputRef.current
    });

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

  return <input ref={inputRef} />;
}

export default DatePicker;

В этом примере:

  • создаётся ссылка на поле ввода;
  • после монтирования компонента инициализируется Pikaday;
  • при размонтировании вызывается метод destroy().

Уничтожение экземпляра является обязательной практикой для предотвращения утечек памяти.


Использование React State

В большинстве приложений выбранная дата должна храниться в состоянии React.

Пример:

import React, { useState, useEffect, useRef } from "react";
import Pikaday from "pikaday";

function DatePicker() {
  const [date, setDate] = useState(null);
  const inputRef = useRef();

  useEffect(() => {
    const picker = new Pikaday({
      field: inputRef.current,

      onSelect() {
        setDate(this.getDate());
      }
    });

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

  return (
    <>
      <input ref={inputRef} />
      <p>{date?.toLocaleDateString()}</p>
    </>
  );
}

После выбора даты:

  1. Pikaday вызывает обработчик onSelect;
  2. выбранное значение извлекается через getDate();
  3. состояние React обновляется через setDate.

Такой подход позволяет использовать выбранную дату в других частях приложения.


Контролируемый компонент

В React широко используются контролируемые поля ввода, значение которых полностью зависит от состояния.

Пример синхронизации состояния и календаря:

import React, { useState, useEffect, useRef } from "react";
import Pikaday from "pikaday";

function DatePicker() {
  const [selectedDate, setSelectedDate] = useState(new Date());

  const inputRef = useRef(null);
  const pickerRef = useRef(null);

  useEffect(() => {
    pickerRef.current = new Pikaday({
      field: inputRef.current,

      onSelect() {
        setSelectedDate(this.getDate());
      }
    });

    return () => pickerRef.current.destroy();
  }, []);

  useEffect(() => {
    if (pickerRef.current) {
      pickerRef.current.setDate(selectedDate, true);
    }
  }, [selectedDate]);

  return (
    <input
      ref={inputRef}
      value={selectedDate.toLocaleDateString()}
      readOnly
    />
  );
}

Здесь React и Pikaday поддерживают единое состояние даты.


Создание переиспользуемого компонента

На практике календарь обычно выносится в отдельный компонент.

Пример универсального компонента:

import React, { useEffect, useRef } from "react";
import Pikaday from "pikaday";

function PikadayInput({
  value,
  onChange,
  options = {}
}) {
  const inputRef = useRef(null);

  useEffect(() => {
    const picker = new Pikaday({
      field: inputRef.current,

      ...options,

      onSelect() {
        onChange(this.getDate());
      }
    });

    if (value) {
      picker.setDate(value);
    }

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

  return (
    <input
      ref={inputRef}
      readOnly
    />
  );
}

export default PikadayInput;

Использование:

<PikadayInput
  value={date}
  onCha nge={setDate}
/>

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


Обновление параметров при изменении props

Иногда параметры календаря должны меняться динамически.

Например, ограничение минимальной даты:

useEffect(() => {
  pickerRef.current.setMinDate(minDate);
}, [minDate]);

Максимальная дата:

useEffect(() => {
  pickerRef.current.setMaxDate(maxDate);
}, [maxDate]);

При изменении свойств React автоматически вызывает соответствующие эффекты и обновляет экземпляр Pikaday.


Передача пользовательских настроек

React-компонент может принимать объект настроек и передавать его в конструктор.

const picker = new Pikaday({
  field: inputRef.current,
  firstDay: 1,
  yearRange: [2020, 2035],
  format: "DD.MM.YYYY"
});

Более гибкий вариант:

const picker = new Pikaday({
  field: inputRef.current,
  ...options
});

Тогда настройки передаются через свойства:

<PikadayInput
  options={{
    firstDay: 1,
    yearRange: [2020, 2035]
  }}
/>

Работа с Formik

Во многих React-проектах применяется библиотека Formik для управления формами.

Пример интеграции:

import { useField } from "formik";

function DateField(props) {
  const [field, meta, helpers] = useField(props.name);

  return (
    <PikadayInput
      value={field.value}
      onCha nge={helpers.setValue}
    />
  );
}

Использование внутри формы:

<DateField name="birthDate" />

После выбора даты значение автоматически сохраняется в состоянии Formik.


Работа с React Hook Form

Интеграция с React Hook Form обычно выполняется через компонент Controller.

import { Controller } from "react-hook-form";

<Controller
  name="date"
  control={control}
  render={({ field }) => (
    <PikadayInput
      value={field.value}
      onCha nge={field.onChange}
    />
  )}
/>

В результате Pikaday становится полноценной частью системы валидации формы.


Использование нескольких календарей

Для выбора диапазона дат часто создаются два экземпляра.

function DateRange() {
  const [startDate, setStartDate] = useState(null);
  const [endDate, setEndDate] = useState(null);

  return (
    <>
      <PikadayInput
        value={startDate}
        onCha nge={setStartDate}
      />

      <PikadayInput
        value={endDate}
        onCha nge={setEndDate}
      />
    </>
  );
}

После выбора начальной даты можно ограничить вторую:

<PikadayInput
  value={endDate}
  onCha nge={setEndDate}
  options={{
    minDate: startDate
  }}
/>

Таким образом реализуется логическая связь между календарями.


Использование в TypeScript

Для TypeScript рекомендуется описывать типы свойств компонента.

interface PikadayInputProps {
  value: Date | null;
  onChange: (date: Date) => void;
}

Компонент:

const PikadayInput: React.FC<PikadayInputProps> = ({
  value,
  onChange
}) => {
  // код компонента
};

Типизация позволяет избежать ошибок при передаче параметров и обеспечивает полноценную поддержку автодополнения.


Серверный рендеринг и Next.js

Pikaday зависит от браузерного DOM и не может корректно работать во время серверного рендеринга.

В проектах на Next.js часто используется динамический импорт:

import dynamic from "next/dynamic";

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

Либо инициализация выполняется только после появления объекта window.

useEffect(() => {
  if (typeof window === "undefined") {
    return;
  }

  const picker = new Pikaday({
    field: inputRef.current
  });

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

Такой подход предотвращает ошибки на стороне сервера.


Оптимизация производительности

При работе с большим количеством календарей полезно соблюдать несколько правил:

  • создавать экземпляр только один раз;
  • не пересоздавать календарь при каждом рендере;
  • хранить объект Pikaday в useRef;
  • уничтожать экземпляры при размонтировании;
  • обновлять только изменившиеся параметры через методы API.

Неэффективный вариант:

useEffect(() => {
  const picker = new Pikaday({
    field: inputRef.current
  });
});

Здесь календарь создаётся после каждого рендера.

Корректный вариант:

useEffect(() => {
  const picker = new Pikaday({
    field: inputRef.current
  });

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

Пустой массив зависимостей гарантирует единственную инициализацию.


Типичные проблемы интеграции

Повторное создание календаря

Причина:

useEffect(() => {
  new Pikaday({
    field: inputRef.current
  });
}, [value]);

Каждое изменение значения приводит к созданию нового экземпляра.

Решение заключается в хранении объекта в useRef и использовании методов setDate, setMinDate, setMaxDate для обновления существующего календаря.

Несинхронизированное состояние

Если React изменяет дату программно, а Pikaday не получает обновление через setDate(), пользователь увидит разные значения в состоянии и интерфейсе.

Необходимо синхронизировать оба источника данных.

Утечки памяти

Отсутствие вызова:

picker.destroy();

может привести к накоплению обработчиков событий после многократного открытия и закрытия компонентов.

Ошибки SSR

При попытке создать экземпляр Pikaday на сервере возникает ошибка отсутствия объекта window или DOM-узлов.

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


Архитектура React-обёртки над Pikaday

На крупных проектах обычно создаётся отдельный слой абстракции:

React UI
    ↓
PikadayWrapper
    ↓
Pikaday API

Такая схема позволяет:

  • скрыть особенности сторонней библиотеки;
  • централизованно управлять настройками;
  • упростить миграцию на другой календарь;
  • обеспечить единый интерфейс для всех форм приложения;
  • повторно использовать компонент во всём проекте.

В результате Pikaday становится полноценным элементом React-экосистемы, сохраняя все возможности оригинальной библиотеки и одновременно подчиняясь принципам управления состоянием и жизненным циклом компонентов React.