Библиотека 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-файла обеспечивает отображение стандартного оформления календаря.
Поскольку 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;
В этом примере:
destroy().Уничтожение экземпляра является обязательной практикой для предотвращения утечек памяти.
В большинстве приложений выбранная дата должна храниться в состоянии 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>
</>
);
}
После выбора даты:
onSelect;getDate();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}
/>
Подобная архитектура позволяет применять компонент в различных частях проекта.
Иногда параметры календаря должны меняться динамически.
Например, ограничение минимальной даты:
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]
}}
/>
Во многих 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 обычно выполняется через компонент
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 рекомендуется описывать типы свойств компонента.
interface PikadayInputProps {
value: Date | null;
onChange: (date: Date) => void;
}
Компонент:
const PikadayInput: React.FC<PikadayInputProps> = ({
value,
onChange
}) => {
// код компонента
};
Типизация позволяет избежать ошибок при передаче параметров и обеспечивает полноценную поддержку автодополнения.
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();
}, []);
Такой подход предотвращает ошибки на стороне сервера.
При работе с большим количеством календарей полезно соблюдать несколько правил:
useRef;Неэффективный вариант:
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();
может привести к накоплению обработчиков событий после многократного открытия и закрытия компонентов.
При попытке создать экземпляр Pikaday на сервере возникает ошибка
отсутствия объекта window или DOM-узлов.
Использование динамического импорта либо проверка среды выполнения полностью устраняют эту проблему.
На крупных проектах обычно создаётся отдельный слой абстракции:
React UI
↓
PikadayWrapper
↓
Pikaday API
Такая схема позволяет:
В результате Pikaday становится полноценным элементом React-экосистемы, сохраняя все возможности оригинальной библиотеки и одновременно подчиняясь принципам управления состоянием и жизненным циклом компонентов React.