こんにちは。カイザーです。最近、フロントエンドの勉強をしており、その中でReact Big Calendarでのタイムゾーン対応について、かなりハマったので共有します。
今回、ユーザが自由に(端末の地域設定とは別に)タイムゾーンを選択できるWEBサイトを構築していたのですが、React Big Calendarでは複数タイムゾーンに対応されていませんでした。
Issueでもタイムゾーン対応について議論されていますが、未だに複数タイムゾーンへの対応が組み込まれる気配がありません。(しかも、なんと3年も議論されています。)
この中で、主な開発者であるjquenseさんは、複数タイムゾーンへの対応をしない理由として「React Big CalendarはViewのレンダリングを行うのが責務であり、タイムゾーンの計算は責務外である」としています。
そのため、利用者からReact Big Calndarをラップすることで、複数タイムゾーンへの対応をするコードが投稿されていたため、そちらを参考に、説明していきます。
これについては、特に難しいことはありません。React Big Calendarへの日時指定はDate型で行いますが、Date型はブラウザのタイムゾーンに依存する形となっているためです。
以下を実行してみると、UTCの2019年5月28日0時0分でイベント登録していますが、端末のタイムゾーンにローカライズされて表示されます。
import React from "react"; import ReactDOM from "react-dom"; import BigCalendar from "react-big-calendar"; import moment from "moment"; import "react-big-calendar/lib/css/react-big-calendar.css"; import "./styles.css"; const localizer = BigCalendar.momentLocalizer(moment); const myEventsList = [ { title: "イベント", start: new Date("28 May 2019 00:00:00 +0000"), // UTCの2019/5/28 0時0分 end: new Date("28 May 2019 10:00:00 +0000") } ]; function App() { return ( <div className="App"> <BigCalendar localizer={localizer} events={myEventsList} startAccessor="start" endAccessor="end" /> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
こちらから実際に実行できます。
私の環境(Asia/Tokyo)で実行してみると、イベントは9時から19時までとなっています。
ブラウザのタイムゾーンに依存すれば良い場合は、これで良いのですが、ブラウザのタイムゾーンに依存せずに、表示する場合は、日時を変換させる必要があります。
ブラウザのタイムゾーンに依存せずに、複数のタイムゾーンに対応させるには、React Big Calendarのラッパを作成し、そこで日時を変換させる必要があります。
日時の変換には「moment」を使用します。
「moment」を使って、日時を変換する関数を作成します。
/** * 日時を指定したタイムゾーンに変換する * quoted from: https://github.com/intljusticemission/react-big-calendar/issues/118#issuecomment-301954768 * @param {*} datetime 変換したい日時 (Date) * @param {*} timeZoneName タイムゾーン名(String) */export const convertDateTimeToDate = (datetime, timeZoneName) => { const m = moment.tz(datetime, timeZoneName); return new Date(m.year(), m.month(), m.date(), m.hour(), m.minute(), 0); };
この関数は、先ほどのIssueに投稿されているプログラムからの引用となります。
momentをタイムゾーン指定で使用することで、Dateを「timeZoneName」引数で指定されたタイムゾーンに変換します。
React Big CalendarはDate型を必要としているため、最後に変換されたmomentの日時を使用して、Date型のインスタンスを返却します。
「端末のタイムゾーンのみに対応する場合」では、BigCalendarへ渡すpropsの「startAccessor」と「endAccessor」に、それぞれ開始日時と終了日時が格納されているキーをStringで指定していました。
今回は、先ほどの「convertDateTimeToDate」関数を使用し、動的に日付を変更するため、propsの「startAccessor」と「endAccessor」を関数呼び出しに変更します。
class BigCalendarTz extends Component { startAccessor = event => { return convertDateTimeToDate(accessor(event, "start"), this.props.timeZone); }; endAccessor = event => { return convertDateTimeToDate(accessor(event, "end"), this.props.timeZone); }; render() { return ( <div className="App"> <BigCalendar localizer={localizer} events={myEventsList} startAccessor={this.startAccessor} // startAccessorはタイムゾーンを動的に変化させるため、関数をセット endAccessor={this.endAccessor} // endAccessorはタイムゾーンを動的に変化させるため、関数をセット /> </div> ); } }
startAccessor関数、endAccessor関数を経由させて、convertDateTimeToDate関数を呼び出しています。
また、BigCalendarに渡すpropsの、「startAccessor」と「endAccessor」に、それぞれ「this.startAccessor」「this.endAccessor」を指定することで、関数が呼び出されるようになります。
これでラップクラス「BigCalendarTz」は完成です。importも含めた全量は以下となります。
import React, { Component } from "react"; import BigCalendar from "react-big-calendar"; import moment from "moment"; import { accessor } from "react-big-calendar/lib/utils/accessors"; import "react-big-calendar/lib/css/react-big-calendar.css"; import "./styles.css"; const localizer = BigCalendar.momentLocalizer(moment); const myEventsList = [ { title: "イベント", start: new Date("28 May 2019 00:00:00 +0000"), // UTCの2019/5/28 0時0分 end: new Date("28 May 2019 10:00:00 +0000") } ]; /** * 日時を指定したタイムゾーンに変換する * quoted from: https://github.com/intljusticemission/react-big-calendar/issues/118#issuecomment-301954768 * @param {*} datetime 変換したい日時 (Date) * @param {*} timeZoneName タイムゾーン名(String) */export const convertDateTimeToDate = (datetime, timeZoneName) => { const m = moment.tz(datetime, timeZoneName); return new Date(m.year(), m.month(), m.date(), m.hour(), m.minute(), 0); }; class BigCalendarTz extends Component { startAccessor = event => { return convertDateTimeToDate(accessor(event, "start"), this.props.timeZone); }; endAccessor = event => { return convertDateTimeToDate(accessor(event, "end"), this.props.timeZone); }; render() { return ( <div className="App"> <BigCalendar localizer={localizer} events={myEventsList} startAccessor={this.startAccessor} // startAccessorはタイムゾーンを動的に変化させるため、関数をセット endAccessor={this.endAccessor} // endAccessorはタイムゾーンを動的に変化させるため、関数をセット /> </div> ); } } export default BigCalendarTz;
呼び出す際は、以下のようになります。
render() { return ( <div className="App"> <BigCalendarTz timeZone={this.state.selectedTimeZone} /> </div> ); }
この場合はstateのselectedTimeZoneに、String型でタイムゾーン名が入っている前提となります。
CodeSandboxで実際に実行できます。
ドロップダウンからタイムゾーンを変更させると、リアクティブにカレンダーのイベントが更新されます。
2019年5月28日を選択した上で、タイムゾーンを変更させると、効果が分かりやすいです。
index.jsでタイムゾーンを選択するドロップダウンを実装しており、その内容をpropsでBigCalendarTzに渡しています。
React Big Calendarで複数タイムゾーンに対応したい場合は、ぜひ試してみてください。