Reactでカレンダーを使う

「react datepicker」でググると、「react-datepicker」のライブラリが出てくると思います。
react-datepickerを使う機会があったので、備忘録にreact-datepickerの機能についてご紹介しようと思います。

## 0. 事前準備
（create-react-appでサンプルを作成しております。）
早速ライブラリをインストールしましょう！

```shell
npm install react-datepicker --save
```

Datepickerを使うコンポーネントにimportさせます。

```javascript
import DatePicker from "react-datepicker"
import "react-datepicker/dist/react-datepicker.css"
```

シンプルなDatepickerの書き方はこうです。

```javascript
const SimpleDatePicker = () => {
  const initialDate = new Date()
  const [startDate, setStartDate] = useState(initialDate)
  const handleChange = (date) => {
    setStartDate(date)
  }

  return (
    <DatePicker
      selected={startDate}
      onChange={handleChange}
    />
  )
}
```

![output2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327745/21fca735-c649-9075-b957-0dcca09035ad.gif)

以下の例では、日付の処理をする関数を作って呼び出すようにしています。

```javascript
/**
 * JST基準に変換して返す
 * @param {string} dateTimeStr YYYY-MM-DDTHH:mm:00Z
 * @returns {moment.Moment}
 */
const parseAsMoment = (dateTimeStr) => {
  return moment.utc(dateTimeStr, 'YYYY-MM-DDTHH:mm:00Z', 'ja').utcOffset(9)
}

/**
 * 日付形式に変換して返す
 * @param {moment.Moment} momentInstance
 * @returns {string}
 */
const toUtcIso8601str = (momentInstance) => {
  return momentInstance
    .clone()
    .utc()
    .format('YYYY-MM-DDTHH:mm:00Z')
}
```

## 1. ある要素をクリックしたらカレンダーを表示させたい場合
[Custom input](https://reactdatepicker.com/#example-44)にDatePickerを表示させるトリガー要素を指定できます。
下の例では`button`にクリックされた日付を表示するようにしています。ボタンにはクリックイベントなど特に指定いりません！

```javascript
const CustomInputDatePicker = () => {
  const [startDate, setStartDate] = useState(toUtcIso8601str(moment()))
  const handleChange = (selectedDate) => {
    setStartDate(toUtcIso8601str(moment(selectedDate)))
  }

  return (
    <DatePicker
      selected={moment(startDate).toDate()}
      onChange={handleChange}
      customInput={
        <button>
          {parseAsMoment(startDate).format('YYYY/MM/DD')}
        </button>
      }
    />
  )
}
```

![output3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327745/8dc8b7aa-133f-2a8c-feaa-4955f520f17e.gif)

## 2. カレンダーのヘッダーを変更したい場合
[Custom header](https://reactdatepicker.com/#example-52)を使うと、カレンダー上の年月を変更できるセレクトボックスと、次月前月への矢印ボタンを自分でカスタマイズすることができます。

```javascript
... // 省略
import getMonth from 'date-fns/getMonth'
import getYear from 'date-fns/getYear'
import _ from 'lodash'
...

const years = _.range(2000, getYear(new Date()) + 1, 1)
const months = Array.from(Array(12).keys())

...

const CustomHeaderDatePicker = () => {
  const [startDate, setStartDate] = useState(toUtcIso8601str(moment()))
  const handleChange = (selectedDate) => {
    setStartDate(toUtcIso8601str(moment(selectedDate)))
  }

  return (
    <DatePicker
      selected={moment(startDate).toDate()}
      onChange={handleChange}
      renderCustomHeader={({
        date,
        changeYear,
        changeMonth,
        decreaseMonth,
        increaseMonth,
        prevMonthButtonDisabled,
        nextMonthButtonDisabled
      }) => (
        <div>
          <button
            onClick={decreaseMonth}
            disabled={prevMonthButtonDisabled}
          >
            前月へ移動
          </button>
          <select
            value={getYear(date)}
            onChange={({ target: { value } }) => changeYear(value)}
          >
            {years.map((option) => (
              <option key={option} value={option}>
                {option}年
              </option>
            ))}
          </select>
          <select
            value={getMonth(date)}
            onChange={({ target: { value } }) => changeMonth(value)}
          >
            {months.map((option) => (
              <option key={option} value={option}>
                {option + 1}月
              </option>
            ))}
          </select>
          <button
            onClick={increaseMonth}
            disabled={nextMonthButtonDisabled}
          >
            次月へ移動
          </button>
        </div>
      )}
    />
  )
}
```

![output4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327745/1f25d0b6-eab8-b051-1829-0bfada60a524.gif)

## 3. カレンダーをラップする時の小ネタ
[Calerndar container](https://reactdatepicker.com/#example-49)内に、カレンダーをラップする要素を指定できます。
見出しやテキスト、他のイベントを設置したい時に使えそうです。
下の例では`calendarContainer`にラップする`<DatetimePickerWrapper />`コンポーネントを指定しています。`children`にカレンダーが入ります。

```javascript
const ContainerDatePicker = () => {
  const [startDate, setStartDate] = useState(toUtcIso8601str(moment()))
  const handleChange = (selectedDate) => {
    setStartDate(toUtcIso8601str(moment(selectedDate)))
  }

  return (
    <DatePicker
      selected={moment(startDate).toDate()}
      onChange={handleChange}
      calendarContainer={DatetimePickerWrapper}
    />
  )
}

const DatetimePickerWrapper = ({ children }) => {
  return (
    <div style={{
      overflow: 'hidden',
      padding: 30,
      backgroundColor: 'skyblue',
      boxShadow: '6px 6px #668AD8',
    }}>
      {children}
    </div>
  )
}
```

![output5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327745/f27595df-5884-a1ce-b463-68ba1212c05f.gif)

## 4. カレンダーの範囲を指定する
公式サイトのDemoにある[Date Range](https://reactdatepicker.com/#example-25)を使うと、日付の範囲を指定できます。
DatePickerを2つ置いて、開始日時を選択する方は`selectsStart`、終了日時を選択する方は`selectsEnd`を指定します。
また、それぞれ日付を選択するchangeイベントを用意し、選択された日にちの`selected`のvalueも開始・終了のステートを指定します。

下のサンプルでは開始日を今日より1週間前、終了日を今日にしています。

```javascript
const DateRangeDatePicker = () => {
  const [startDate, setStartDate] = useState(toUtcIso8601str(moment().subtract(7, 'days')))
  const [endDate, setEndDate] = useState(toUtcIso8601str(moment()))
  const handleChangeStart = (selectedDate) => {
    setStartDate(toUtcIso8601str(moment(selectedDate)))
  }
  const handleChangeEnd = (selectedDate) => {
    setEndDate(toUtcIso8601str(moment(selectedDate)))
  }

  return (
    <Fragment>
      <DatePicker
        selected={moment(startDate).toDate()}
        selectsStart
        startDate={moment(startDate).toDate()}
        endDate={moment(endDate).toDate()}
        onChange={handleChangeStart}
      />
      <DatePicker
        selected={moment(endDate).toDate()}
        selectsEnd
        startDate={moment(startDate).toDate()}
        endDate={moment(endDate).toDate()}
        onChange={handleChangeEnd}
      />
    </Fragment>
  )
}
```

[Inline](https://reactdatepicker.com/#example-40)を指定するとカレンダーがベタ表示されるようになるので、使い勝手がよくなりそうです！

![output6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327745/beb61364-5889-4000-43a6-ff6d08eab853.gif)

## 5. カレンダーを日本語対応させる
[Local](https://reactdatepicker.com/#example-17)で`"ja"`を指定すると、日本語になります。

```javascript
... // 省略
import DatePicker, { registerLocale } from 'react-datepicker'
import ja from 'date-fns/locale/ja'
registerLocale('ja', ja)
...
const LocalDatePicker = () => {
  const [startDate, setStartDate] = useState(toUtcIso8601str(moment()))
  const handleChange = (selectedDate) => {
    setStartDate(toUtcIso8601str(moment(selectedDate)))
  }

  return (
    <Fragment>
      <DatePicker
        locale="ja"
        selected={moment(startDate).toDate()}
        onChange={handleChange}
      />
    </Fragment>
  )
}
```

![output7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327745/33d7696c-5bc1-0790-69a7-d1a229861847.gif)

## 6. カレンダーの複数表示
カレンダーを２ヶ月続けて横並び表示させる場合は[Multiple months](https://reactdatepicker.com/#example-45)を使います。

下の例では「今月」「先月」をクリックした時、開始・終了日を`setState`してハイライトさせています。
※注意：下の例はChangeイベント系を除いて書いています！`monthsShown`、`inline`等の使い方だけご参考ください。

```javascript
const MultipleDatePicker = () => {
  const [startDate, setStartDate] = useState(toUtcIso8601str(moment().subtract(7, 'days')))
  const [endDate, setEndDate] = useState(toUtcIso8601str(moment()))

  ... // 省略

  return (
    <div>
      <p>開始日: {parseAsMoment(startDate).format('YYYY/MM/DD')}</p>
      <p>終了日: {parseAsMoment(endDate).format('YYYY/MM/DD')}</p>
      <DatePicker
        inline
        selected={moment(startDate).toDate()}
        startDate={moment(startDate).toDate()}
        endDate={moment(endDate).toDate()}
        onChange={handleChange}
        monthsShown={2}
        calendarContainer={({ children }) => (
          <div>
            <div style={{ marginBottom: 20 }}>
              <button onClick={() => handleChangeDateRange('thisMonth')}>今月</button>
              <button onClick={() => handleChangeDateRange('lastMonth')}>先月</button>
            </div>
            {children}
          </div>
        )}
      />
    </div>
  )
}
```

![output8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327745/2d9e8f57-e38a-b5bf-772f-03397806a328.gif)

## 終わりに
「こんなオプションも用意してくれてるのか！」と感動したので、備忘録として残しました笑。
上記で紹介した指定をした上でスタイルを整えてあげると、下のような感じで良い感じのカレンダーが出来上がります！

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327745/12d48817-8e98-3c97-7549-4c11703aa85f.gif)