はじめに
前回で、予定の追加・削除・保存(localStorage)まで一通りの機能が揃った。ただし、画面はずっと「予定の一覧が並ぶだけ」のシンプルな見た目のままだった。
今回は、いよいよ「予定管理アプリ」らしい見た目に近づける。月表示のカレンダーを実装し、カレンダー上の日付をクリックすると、その日の予定だけが表示・追加・削除できるようにする。
今回もツールが生成したpackage.jsonなどのファイルは変更していない。読むべきは、新しく追加するCalendar.jsxと、変更が入るApp.jsx・ScheduleForm.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つとも共有する必要がある。そこで、この情報は親であるAppがuseStateで持ち、必要なコンポーネントにpropsとして渡す。
src/App.jsx(抜粋)
const [selectedDate, setSelectedDate] = useState(todayString)
<Calendar
selectedDate={selectedDate}
onSelectDate={setSelectedDate}
...
/>
<ScheduleForm selectedDate={selectedDate} onAdd={handleAdd} />
カレンダー側で日付がクリックされると、onSelectDate(実体はsetSelectedDate)が呼ばれ、Appの状態が更新される。状態が変わると、Calendar・ScheduleForm・予定一覧のすべてが、新しい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



コメント