カテゴリー: FrontEnd

React Big Calendarの複数タイムゾーン対応

はじめに

こんにちは。カイザーです。最近、フロントエンドの勉強をしており、その中で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;

ラップクラス「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で複数タイムゾーンに対応したい場合は、ぜひ試してみてください。

おすすめ書籍

   

カイザー

シェア
執筆者:
カイザー

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

4週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前