はじめに
・Reactで予約機能を実装する際、FullCalendarは非常に強力なライブラリです。
・本記事では、Fullcalendarを使用し、週表示及び月表示のカレンダーを実装する際のロジックを解説します。
・UIのコード等は割愛し解説しています。
前提知識(これを知っていると理解度が変わる)
この記事は、以下の知識がある前提で解説をしております。
・Reactの基礎: コンポーネント、props、Stateの概念。
・React Hooks: useState, useEffect, useRef, useMemo の基本的な使用方法
・TypeScript: 型定義の基本的な書き方
ここだけは覚えて帰ろう
本記事の要点は以下の4つです。
1. useRef:カレンダーを外から操作する「リモコン」
カレンダーの「次の月へ」ボタンを自作したいとき、どうやってカレンダー自体に「動いて!」と命令すればいいでしょうか?
useRef は、まさにそのためのリモコンを手に入れるようなものです。このリモコン(.getApi())を使えば、「次の月へ進め (next())」「指定した日にジャンプしろ (gotoDate())」といった命令をカレンダーに送ることができます。
2. dayCellContent:日付を飾り付けする「デザイナー」
カレンダーの日付ひとつひとつ(セル)の見た目を自由に変えたいときに使います。「日曜日は赤色にしたい」「予約で埋まっている日はグレーにしたい」といった要望を叶えるのが dayCellContent です。これはカレンダー専門のデザイナーのようなもので、「こういう条件の日付には、こういう見た目(CSS)にしてください」という指示書(関数)を渡すだけで、自動で日付を飾り付けしてくれます。
3. 状態管理:データの置き場所を賢く分ける
ユーザーが選んだ日付や時間といった「データ」をどこに保存しておくかは重要です。
useState は「コンポーネント内のメモ帳」
カレンダー上で「今クリックしている日付」のように、その部品の中でだけ一時的に使いたい情報は useState にメモしておきます。
Props は「親から子への連絡帳」
予約フォーム全体で「最終的に決定した日時」のように、複数の部品で共有したい大切な情報は、親コンポーネントから Props という形で渡してもらいます。
このようにデータの役割に応じて置き場所を分けることで、コードが整理され、管理しやすくなります。
4. useMemo:面倒な計算をサボるための「賢いメモ帳」
「4月10日に予約できる時間帯」を探すとき、毎回すべての予約データの中から探し出すのは大変です。useMemo は、一度計算した結果をメモしておき、条件が変わらない限りはそのメモを使い回す賢い仕組みです。ユーザーが日付をクリックするたびに重い計算が走るのを防ぎ、アプリがサクサク動くようになります。いわば計算をサボって楽するためのテクニックです。
FullCalendarの採用理由
- 工数削減: カレンダーの複雑なロジックをライブラリが吸収してくれるため、開発者は本質的な機能実装に集中できる
- 高い拡張性: プラグイン機能を追加するだけで、週表示、月表示、タイムライン機能等多彩なビューに簡単に対応可能
- 優れた保守性: Reactのコンポーネント設計と親和性が高く、ロジックがシンプルになるため、コードの可読性、保守性が向上する
実装のポイント解説(ロジック編)
・まず、プロジェクトに必要なライブラリをインストールします。お使いのパッケージマネージャーに合わせて、以下のいずれかのコマンドを実行してください。
// npm をお使いの場合
npm install @fullcalendar/react @fullcalendar/daygrid @fullcalendar/interaction
// yarn をお使いの場合
yarn add @fullcalendar/react @fullcalendar/daygrid @fullcalendar/interaction
・次にコンポーネント間で共有する型を定義しておくと便利です
// 選択された日付と時間の型
export type SelectionValue = {
date: string | null;
time: string | null;
};
// APIなどから渡されるイベントデータの型
export type Event = {
date: string;
time: string;
};
Case1: 週表示カレンダーの実装ロジック
ポイント
・initialView="dayGridWeek" を指定。(週表示)
・useRef でカレンダーのインスタンスにアクセスし、prev() / next() メソッドで週を移動させます。
・dayCellContent を使って、日付セルの見た目を曜日や選択状態で動的に変更します。
import React, { useRef, useState } from "react";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin, { type DateClickArg } from "@fullcalendar/interaction";
import type { DayCellContentArg } from "@fullcalendar/core";
const WeeklyCalendar = () => {
// FullCalendarのコンポーネントを直接操作するための「リモコン」の入れ物を作成
const calendarRef = useRef<FullCalendar>(null);
// ユーザーがクリックした日付を覚えておくための状態(State)
// 初期値は何も選択されていないので null
const [selectedDate, setSelectedDate] = useState<string | null>(null);
// 日付がクリックされたときにFullCalendarから呼び出される関数
const handleDateClick = (arg: DateClickArg) => {
// クリックされた日付の文字列('2025-09-29'など)をstateに保存する
setSelectedDate(arg.dateStr);
};
// 「前の週へ」ボタンが押されたときに呼ぶ関数
const goToPrevWeek = () => {
// calendarRef(リモコン)を使って、FullCalendarのAPIを呼び出し、表示を前の週に動かす
calendarRef.current?.getApi().prev();
};
// 「次の週へ」ボタンが押されたときに呼ぶ関数
const goToNextWeek = () => {
// 同様に、表示を次の週に動かす
calendarRef.current?.getApi().next();
};
// 各日付セルの見た目をカスタマイズするための関数
const renderDayCellContent = (arg: DayCellContentArg) => {
// このセルが表示しようとしている日付が、現在選択中の日付(state)と同じかどうかをチェック
const isSelected = arg.dateStr === selectedDate;
// ここで返したJSXが、日付セルのHTMLとしてレンダリングされる
// isSelectedがtrueなら、選択中を示すCSSクラスを適用する
return (
<div className={isSelected ? "bg-blue-500 text-white" : ""}>
{arg.dayNumberText}
</div>
);
};
return (
<div>
{/* ... UI部分は割愛 ... */}
<button onClick={goToPrevWeek}>前の週</button>
<button onClick={goToNextWeek}>次の週</button>
<FullCalendar
// 作成したリモコンの入れ物をFullCalendarに渡す
ref={calendarRef}
// 使用するプラグイン(機能拡張)を指定
plugins={[dayGridPlugin, interactionPlugin]}
// カレンダーの初期表示の種類を「週表示」に設定
initialView="dayGridWeek"
// 表示言語を日本語に
locale="ja"
// デフォルトのヘッダー(年月や移動ボタン)は自作するので非表示に
headerToolbar={false}
// 日付がクリックされたら、handleDateClick関数を呼び出すように紐付け
dateClick={handleDateClick}
// 日付セルの見た目を、renderDayCellContent関数で作るように紐付け
dayCellContent={renderDayCellContent}
/>
</div>
);
};
Case2: 月表示カレンダーの実装ロジック
ポイント
・initialView="dayGridMonth" を指定。(月表示)
・useMemo を使って、予約可能な日付リスト(availableDates)を事前に計算し、パフォーマンスを向上させます。
・dayCellContent 内で、予約可能か、選択中かを判定し、見た目を細かく制御します。
import React, { useState, useMemo } from "react";
import FullCalendar from "@fullcalendar/react";
// (各種importは省略)
import { Event } from "../types"; // 型をインポート
// 親コンポーネントから予約可能なイベント情報の配列を受け取る
type Props = { events: Event[] };
const MonthlyCalendar: FC<Props> = ({ events }) => {
// 月表示カレンダー内で、ユーザーがクリックした日付を覚えておくための状態
const [selectedDate, setSelectedDate] = useState<string | null>(null);
// 親から受け取った `events` データから、予約が可能な日付だけを抜き出してSetを作成する
// useMemoを使うことで、`events`データが変更されない限り、この重い計算を再実行しないようにしている(パフォーマンス最適化)
const availableDates = useMemo(() => {
// Setを使うと、重複する日付が自動でなくなり、特定の日付が含まれているかのチェック(.has())が高速になる
return new Set(events.map((event) => event.date));
}, [events]);
// 各日付セルの見た目をカスタマイズするための関数
const renderDayCellContent = (arg: DayCellContentArg) => {
// このセルが描画しようとしている日付の文字列
const { dateStr } = arg;
// この日付が、予約可能な日付リストに含まれているかチェック
const isAvailable = availableDates.has(dateStr);
// この日付が、現在ユーザーによって選択されているかチェック
const isSelected = dateStr === selectedDate;
// isAvailableやisSelectedの結果に応じて、見た目を変更するためのCSSクラスを動的に割り当てる
// (具体的なJSXの実装は、要件に合わせて変更)
// ... isAvailable, isSelectedに応じたスタイル変更ロジック ...
};
// 日付がクリックされたときに呼び出される関数
const handleDateClick = (arg: DateClickArg) => {
// 予約可能な日付でなかった場合は、ここで処理を終了し、何も起こさない
if (!availableDates.has(arg.dateStr)) {
return;
}
// 予約可能な日付の場合のみ、その日付をstateに保存する
setSelectedDate(arg.dateStr);
};
return (
<FullCalendar
// 使用するプラグインを指定
plugins={[dayGridPlugin, interactionPlugin]}
// カレンダーの初期表示の種類を「月表示」に設定
initialView="dayGridMonth"
// 表示言語を日本語に
locale="ja"
// 日付がクリックされたら、handleDateClick関数を呼び出すように紐付け
dateClick={handleDateClick}
// 日付セルの見た目を、renderDayCellContent関数で作るように紐付け
dayCellContent={renderDayCellContent}
/>
);
};
実践: 週表示と月表示の連結とUX向上テクニック
・実際のアプリケーションでは、これらのレビューを連結させ、ユーザが迷わないようにUXを高める工夫が必要です。
1. コンポーネント間のデータ連結
・「普段は週表示で、ユーザーが日付を広く探したいときだけ月表示モーダルを開く」というUIを、コールバック関数を使って実現します。
- 親(週表示コンポーネント)が、月表示モーダルを呼び出す。
- モーダル内で日時が確定されたら、
onConfirmのようなコールバック関数で親に値を渡す。 - 親は受け取った日付に
gotoDateで移動する。
※例: components/CalendarWrapper.tsx (抜粋)
const CalendarWrapper = () => {
const calendarRef = useRef<FullCalendar>(null);
// ... (state定義) ...
// モーダルから日時が確定されたときの処理
const handleConfirmFromModal = (selection: SelectionValue) => {
// 1. 親の選択状態を更新
onValueChange(selection);
// 2. 週表示カレンダーをその日付に移動
if (selection.date) {
calendarRef.current?.getApi().gotoDate(selection.date);
}
// 3. モーダルを閉じる
setIsModalOpen(false);
};
return (
<>
{/* ... 週表示カレンダー ... */}
<MonthlyCalendarModal
isOpen={isModalOpen}
onConfirm={handleConfirmFromModal} // コールバックを渡す
/>
</>
);
};
2. モーダル内のUX向上テクニック
・月表示モーダルでは、ユーザーが無駄な操作をしないよう、過去の日付を選択できないようにします。
・FullCalendarの validRange オプションを使えば、カレンダー全体で選択可能な期間を簡単に制限できます。
例: components/MonthlyCalendarModal.tsx (抜粋)
const MonthlyCalendarModal = ({ events }) => {
// ...
const todayStr = useMemo(() => new Date().toISOString().split("T")[0], []);
return (
<FullCalendar
// ... (その他オプション)
// startに今日の日付文字列を指定することで、それより過去の日は操作不能になる
validRange={{ start: todayStr }}
/>
);
};
・これを設定するだけで、過去月への移動や日付クリックができなくなり、ユーザー体験が大きく向上します。
・予約枠がない日をクリックしても何も起こらないように、イベントハンドラ側で制御します。
例: components/MonthlyCalendarModal.tsx (抜粋)
const MonthlyCalendarModal = ({ events }) => {
const availableDates = useMemo(() => new Set(events.map(e => e.date)), [events]);
const handleDateClick = (arg: DateClickArg) => {
// 予約できない日付の場合は、処理を中断する (ガード節)
if (!availableDates.has(arg.dateStr)) {
return;
}
// 予約可能な日付の場合のみ、選択状態を更新
setSelectedDate(arg.dateStr);
};
return <FullCalendar dateClick={handleDateClick} /* ... */ />;
};
・これらの小さな工夫が、アプリケーション全体の使いやすさに繋がります。
おわりに
・FullCalendarの主要なロジックと、それを組み合わせた実践的なUX向上テクニックを解説しました。
・useRef でのAPI操作、dayCellContent でのUIカスタマイズ、そして validRange などの便利なオプションがあり、非常に拡張性が高く使用できます。
・この記事が、あなたのカレンダー実装の助けとなれば幸いです。