0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React + FullCalendar】週 / 月 表示カレンダーの主要ロジックについて

0
Posted at

はじめに

・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の採用理由

  1. 工数削減: カレンダーの複雑なロジックをライブラリが吸収してくれるため、開発者は本質的な機能実装に集中できる
  2. 高い拡張性: プラグイン機能を追加するだけで、週表示、月表示、タイムライン機能等多彩なビューに簡単に対応可能
  3. 優れた保守性: 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を、コールバック関数を使って実現します。

  1. 親(週表示コンポーネント)が、月表示モーダルを呼び出す。
  2. モーダル内で日時が確定されたら、onConfirm のようなコールバック関数で親に値を渡す。
  3. 親は受け取った日付に 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 などの便利なオプションがあり、非常に拡張性が高く使用できます。
・この記事が、あなたのカレンダー実装の助けとなれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?