
React + MUIで日付の入力フォームを作成する際にはDatePickerがよく使用されます。

DatePickerで開始日と終了日の入力フォームを普通に作成したコードは以下のようになります。
import { useState } from 'react'
import { Dayjs } from 'dayjs'
import 'dayjs/locale/ja'
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import { DatePicker } from '@mui/x-date-pickers/DatePicker'
import { Box } from '@mui/material'
export default function DatePickerValue() {
const [dateStart, setDateStart] = useState<Dayjs | null>(null)
const [dateEnd, setDateEnd] = useState<Dayjs | null>(null)
// 開始日が選択された後に終了日のカレンダーを開く
const handleChangeStart = (newDate: Dayjs | null) => {
setDateStart(newDate)
}
// 終了日が選択された後にカレンダーを閉じる
const handleChangeEnd = (newDate: Dayjs | null) => {
setDateEnd(newDate)
}
return (
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ja">
<Box display="flex" gap="1em">
<DatePicker
label="開始日"
value={dateStart}
onChange={handleChangeStart}
format="YYYY/MM/DD"
/>
<DatePicker
label="終了日"
value={dateEnd}
onChange={handleChangeEnd}
format="YYYY/MM/DD"
/>
</Box>
</LocalizationProvider>
)
}
@mui/materialやdayjsなどを使用しているので、React環境を構築後に以下のコマンドでインストールが必要です。
npm install @mui/material @emotion/react @emotion/styled @mui/x-date-pickers dayjs
開始日と終了日のような期間の入力にはDateRangePickerを使うと入力しやすいですが、有料のため誰でも使用できるわけではありません。
そこで、無料でDatePickerを使用して開始日と終了日を入力しやすくする方法について解説します。
DatePickerの日付の期間入力は使いづらい
DateRangePickerではなくDatePickerで日付の期間の入力部分を作成すると、3つの問題が発生するため使いづらくなってしまいます。
- 開始日入力後に終了日もクリックして開かなくてはならない
- 間違えて終了日に開始日より前の日付を入力してしまう可能性がある
- 開始日を終了日より後の日付に設定し直しても終了日は変わらない
そのため、開始日と終了日の2つの入力箇所がある場合はDatePicker同士を連動させて、入力の手間とミスが減るようにカスタマイズしたほうが良いです。
具体的には開始日を入力後に終了日を自動で開くようにして、終了日は開始日より前の日付は選択不可にします。

DatePickerの改良手順
まず、終了日には開始日より前の日付が入力可能になっているので、DatePickerにminDate属性を追加し、終了日に開始日より前の日付が入力できないようにします。
これは終了日のDatePickerに「minDate={dateStart || undefined}」を追加するだけで実装できます。
<DatePicker
label="終了日"
value={dateEnd}
onChange={handleChangeEnd}
format="YYYY/MM/DD"
minDate={dateStart || undefined}
/>
これだけだと開始日のほうをあとから終了日よりあとの日付にした際に矛盾が生じるので、終了日よりあとの開始日が選択されたら、終了日をリセットする処理もhandleChangeStartに追加します。
// 開始日が選択された後に終了日のカレンダーを開く
const handleChangeStart = (newDate: Dayjs | null) => {
setDateStart(newDate)
// 終了日よりあとの開始日が選択されたら、終了日をリセットする
if (newDate && dateEnd && dayjs(dateEnd).isBefore(newDate)) {
setDateEnd(null)
}
}
DatePickerにはopen属性というものがあり、open={true} だと日付入力のカレンダーが表示され、open={false} だと日付入力のカレンダーが非表示になります。
そのため、open属性の部分は変数(useState)を使えば表示・非表示を切り替えることができます。
DatePickerには日付カレンダーの表示・非表示のイベントを操作するonOpenやonClose属性もあるので、これらを利用して状態管理すれば、開始日を入力後に自動で終了日のカレンダーを開くことができます。
import { useState } from 'react'
import dayjs, { Dayjs } from 'dayjs'
import 'dayjs/locale/ja'
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import { DatePicker } from '@mui/x-date-pickers/DatePicker'
import { Box } from '@mui/material'
export default function DatePickerValue() {
const [dateStart, setDateStart] = useState<Dayjs | null>(null)
const [dateEnd, setDateEnd] = useState<Dayjs | null>(null)
const [openEndDatePicker, setOpenEndDatePicker] = useState(false)
// 開始日が選択された後に終了日のカレンダーを開く
const handleChangeStart = (newDate: Dayjs | null) => {
setDateStart(newDate)
// 終了日よりあとの開始日が選択されたら、終了日をリセットする
if (newDate && dateEnd && dayjs(dateEnd).isBefore(newDate)) {
setDateEnd(null)
}
// 開始日が選択されたときに終了日のカレンダーを開く
setOpenEndDatePicker(true)
}
// 終了日が選択された後にカレンダーを閉じる
const handleChangeEnd = (newDate: Dayjs | null) => {
setDateEnd(newDate)
}
// 終了日のカレンダーを開いたときにopenEndDatePickerをtrueにする
const handleOpenEndDatePicker = () => {
setOpenEndDatePicker(true)
}
return (
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ja">
<Box display="flex" gap="1em">
<DatePicker
label="開始日"
value={dateStart}
onChange={handleChangeStart}
format="YYYY/MM/DD"
/>
<DatePicker
label="終了日"
value={dateEnd}
onChange={handleChangeEnd}
format="YYYY/MM/DD"
minDate={dateStart || undefined}
open={openEndDatePicker}
onOpen={handleOpenEndDatePicker}
onClose={() => setOpenEndDatePicker(false)}
/>
</Box>
</LocalizationProvider>
)
}

※ サンプルのリンク先の入力フォームはStackBlitzだと表示に35秒ほど時間がかかります。