Reactで学ぶ 予定管理アプリ開発(第5回)~月表示カレンダーの実装~

パソコン GitHubでつくって学ぶ

はじめに

前回で、予定の追加・削除・保存(localStorage)まで一通りの機能が揃った。ただし、画面はずっと「予定の一覧が並ぶだけ」のシンプルな見た目のままだった。

今回は、いよいよ「予定管理アプリ」らしい見た目に近づける。月表示のカレンダーを実装し、カレンダー上の日付をクリックすると、その日の予定だけが表示・追加・削除できるようにする。

今回もツールが生成したpackage.jsonなどのファイルは変更していない。読むべきは、新しく追加するCalendar.jsxと、変更が入るApp.jsxScheduleForm.jsxである(ScheduleItem.jsxはVer.4から変更なし)。

今回やること

  • Dateオブジェクトを使って、月ごとのカレンダーのマス目を組み立てる
  • カレンダー上の日付をクリックすると、その日を「選択中」として状態に持つ
  • 選択中の日付に応じて、予定の表示・追加・削除の対象を絞り込む

カレンダーのマス目をどう作るか

カレンダーを描画するには、「その月の1日が何曜日から始まるか」と「その月が何日まであるか」が分かればよい。JavaScriptのDateオブジェクトを使うと、この2つを簡単に求められる。

const firstDay = new Date(year, month, 1).getDay()      // 1日の曜日(0=日曜、1=月曜…)
const lastDate = new Date(year, month + 1, 0).getDate() // その月の最終日

new Date(year, month + 1, 0)という書き方は少し分かりにくいかもしれない。「翌月の0日目」を指定すると、結果的に「今月の最終日」になる、というJavaScriptのDateの仕様を利用したものである。

この2つの値をもとに、「月初までの空白セル」と「1日から最終日までの日付セル」を並べた配列を作る。

src/components/Calendar.jsx(抜粋)

function buildCalendarCells(year, month) {
  const firstDay = new Date(year, month, 1).getDay()
  const lastDate = new Date(year, month + 1, 0).getDate()

  const cells = []

  for (let i = 0; i < firstDay; i += 1) {
    cells.push(null) // 月初までの空白セル
  }

  for (let day = 1; day <= lastDate; day += 1) {
    cells.push(day) // 日付セル
  }

  return cells
}

この配列を7列(日〜土)のグリッドとして並べれば、見慣えたカレンダーの形になる。

日付ごとの予定件数を集計する

カレンダーの各マスに「その日に予定が何件あるか」を表示したい。これは、配列の.reduce()を使うと簡潔に書ける。

const scheduleCountByDate = schedules.reduce((acc, schedule) => {
  acc[schedule.date] = (acc[schedule.date] || 0) + 1
  return acc
}, {})

schedules(予定の配列)を1件ずつ見ていき、{ "2026-07-20": 1, "2026-07-21": 2, ... }のような「日付をキーにした件数の集計」を作っている。.reduce()は、配列を1つの値(今回はオブジェクト)にまとめたい時に使うメソッドである。

選択した日付を状態として持つ

カレンダー、フォーム、予定一覧は別々のコンポーネントだが、「今どの日付が選ばれているか」という情報は、3つとも共有する必要がある。そこで、この情報は親であるAppuseStateで持ち、必要なコンポーネントにpropsとして渡す。

src/App.jsx(抜粋)

const [selectedDate, setSelectedDate] = useState(todayString)
<Calendar
  selectedDate={selectedDate}
  onSelectDate={setSelectedDate}
  ...
/>

<ScheduleForm selectedDate={selectedDate} onAdd={handleAdd} />

カレンダー側で日付がクリックされると、onSelectDate(実体はsetSelectedDate)が呼ばれ、Appの状態が更新される。状態が変わると、CalendarScheduleForm・予定一覧のすべてが、新しいselectedDateに応じて再描画される。「1つの状態を、複数のコンポーネントで共有する」という、Reactでよく使う考え方である。

選択日でフィルタリングする

予定一覧は、全件表示ではなく、選択中の日付のものだけに絞り込む。

const schedulesForSelectedDate = schedules.filter(
  (schedule) => schedule.date === selectedDate
)

Ver.3で削除処理に使った.filter()と同じ仕組みで、今回は「選択中の日付と一致するものだけ残す」という条件にしている。

フォームの変更点

Ver.4までのScheduleFormは日付とタイトルの両方を入力する形だったが、今回はカレンダーで選んだ日付が自動的に使われるため、フォーム側ではタイトルだけを入力すればよいようにした。

src/components/ScheduleForm.jsx

import { useState } from 'react'

function ScheduleForm({ selectedDate, onAdd }) {
  const [title, setTitle] = useState('')

  const handleSubmit = (event) => {
    event.preventDefault()
    if (!title) return

    onAdd({ date: selectedDate, title })
    setTitle('')
  }

  return (
    <form className="schedule-form" onSubmit={handleSubmit}>
      <span className="schedule-form__date">{selectedDate}</span>
      <input
        type="text"
        value={title}
        onChange={(event) => setTitle(event.target.value)}
        placeholder="予定のタイトル"
        required
      />
      <button type="submit">追加</button>
    </form>
  )
}

export default ScheduleForm

日付の入力欄がなくなった代わりに、選択中の日付をそのまま表示する<span>に置き換えている。

実行結果

npm run devで開発サーバーを起動し、ブラウザで確認する。終了する場合はターミナル上でCtrl + Cを押す(Ctrl + Zは一時停止するだけなので使わない)。

カレンダー上の日付をクリックすると、その日の予定だけが下に表示される。予定が登録されている日には、件数を示す印が付く。前月・翌月への移動もできる。

今回のまとめ

  • Dateオブジェクトを使うと、月初の曜日・最終日を求めてカレンダーのマス目を組み立てられる
  • .reduce()を使うと、配列から「日付ごとの件数」のような集計データを作れる
  • 複数のコンポーネントで共有したい状態(選択中の日付など)は、親コンポーネントがuseStateで持ち、propsで配る

これで予定管理アプリらしい見た目と機能が一通り揃った。次回は最終回として、このアプリをGitHub Pagesにデプロイし、実際にインターネット上で誰でも使えるようにする。

ソースコード

今回のソースコードは、私のGitHubで公開している。以下のリンクを参照していたただきたい。

GitHub:https://github.com/HappyTalk10/pc-labo-react-learning/tree/main/05_calendar-app


コメント

タイトルとURLをコピーしました