React×Google Calendarでオリジナルタスク管理アプリを作った話

Google Calendarのデータを使って、自分好みのタスク管理アプリを作りたい——そう思ったのは、既存のタスクアプリに限界を感じたからだ。

TodoistもNotionも試した。でも「カレンダーと完全同期しながら、独自のビューで作業を管理する」という要件を満たすツールがなかった。だったら作ればいい。

なぜGoogle Calendar APIを選んだか

タスク管理アプリを自作するとき、データの保存先は大きな選択肢だ。独自DBを持つか、既存サービスのAPIを使うか。

Google Calendar APIを選んだ理由は3つある。

1. すでにデータがある — 仕事の予定はずっとGoogleカレンダーに入れていたので、タスクと予定が同一データソースに乗る

2. 無料枠が十分 — APIの無料クォータは個人利用には余裕がある

3. モバイルアプリとの同期が自動 — iPhoneのカレンダーアプリと自動同期されるので、外出先でも確認できる

技術スタック

フロントエンド: React 18 + Vite
スタイリング: Tailwind CSS
API連携: Google Calendar API v3
認証: OAuth 2.0(Google Identity Services)
状態管理: useState + useContext(小規模なので)
ホスティング: Vercel

Google Calendar APIのセットアップ

OAuth 2.0の設定

Google Cloud Consoleでプロジェクトを作り、Calendar APIを有効化する。OAuthクライアントIDを発行する際、承認済みのJavaScriptオリジンにローカル開発URL(http://localhost:5173)と本番URL(https://your-app.vercel.app)を追加する。

// src/lib/googleAuth.js
const CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const SCOPES = 'https://www.googleapis.com/auth/calendar';

export function initGoogleAuth(onSuccess, onError) {
  google.accounts.oauth2.initTokenClient({
    client_id: CLIENT_ID,
    scope: SCOPES,
    callback: (response) => {
      if (response.error) {
        onError(response.error);
        return;
      }
      localStorage.setItem('gapi_token', response.access_token);
      onSuccess(response.access_token);
    },
  });
}

カレンダーイベントの取得

// src/lib/calendarApi.js
const BASE_URL = 'https://www.googleapis.com/calendar/v3';

export async function fetchEvents(calendarId = 'primary', options = {}) {
  const token = localStorage.getItem('gapi_token');
  const { timeMin, timeMax, maxResults = 100 } = options;

  const params = new URLSearchParams({
    maxResults,
    orderBy: 'startTime',
    singleEvents: true,
    ...(timeMin && { timeMin }),
    ...(timeMax && { timeMax }),
  });

  const res = await fetch(
    `${BASE_URL}/calendars/${encodeURIComponent(calendarId)}/events?${params}`,
    {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    }
  );

  if (!res.ok) {
    throw new Error(`Calendar API error: ${res.status}`);
  }

  const data = await res.json();
  return data.items;
}

Reactコンポーネントの設計

タスク一覧とカレンダーの同期

アプリの核心は「カレンダーイベント」と「タスク」を同一データとして扱う部分だ。

Google Calendarのイベントに独自プロパティ(extendedProperties)を持たせることで、タスクのステータス(完了/未完了)や優先度を保存できる。

// src/hooks/useTasks.js
import { useState, useEffect, useCallback } from 'react';
import { fetchEvents, updateEvent, createEvent } from '../lib/calendarApi';

export function useTasks(calendarId) {
  const [tasks, setTasks] = useState([]);
  const [loading, setLoading] = useState(false);

  const loadTasks = useCallback(async () => {
    setLoading(true);
    try {
      const now = new Date();
      const events = await fetchEvents(calendarId, {
        timeMin: new Date(now.getFullYear(), now.getMonth(), 1).toISOString(),
        timeMax: new Date(now.getFullYear(), now.getMonth() + 2, 0).toISOString(),
      });

      // extendedProperties.private.task_type === 'task' のもののみ抽出
      const taskEvents = events.filter(
        (e) => e.extendedProperties?.private?.task_type === 'task'
      );
      setTasks(taskEvents);
    } finally {
      setLoading(false);
    }
  }, [calendarId]);

  useEffect(() => {
    loadTasks();
  }, [loadTasks]);

  const completeTask = async (taskId) => {
    const task = tasks.find((t) => t.id === taskId);
    if (!task) return;

    const updated = await updateEvent(calendarId, taskId, {
      extendedProperties: {
        private: {
          ...task.extendedProperties?.private,
          status: 'done',
        },
      },
    });

    setTasks((prev) => prev.map((t) => (t.id === taskId ? updated : t)));
  };

  return { tasks, loading, reload: loadTasks, completeTask };
}

タスクカードコンポーネント

// src/components/TaskCard.jsx
export function TaskCard({ task, onComplete }) {
  const isDone = task.extendedProperties?.private?.status === 'done';
  const priority = task.extendedProperties?.private?.priority || 'medium';

  const priorityColor = {
    high: 'border-red-400',
    medium: 'border-yellow-400',
    low: 'border-green-400',
  }[priority];

  return (
    

{task.summary}

{task.description && (

{task.description}

)}

{new Date(task.start.dateTime || task.start.date).toLocaleDateString('ja-JP')}

{!isDone && ( )}
); }

実際に使ってみてわかったこと

よかった点

カレンダービューとタスクビューの切り替えが便利。同じデータをカレンダー表示でもリスト表示でも見られる。「今週やること」を週次ビューで一覧するのが特に気持ちいい。

iPhoneのカレンダーアプリに自動で出る。わざわざアプリを開かなくても、通知で今日のタスクを確認できる。

苦労した点

OAuth 2.0のトークン管理が一番ハマった。access_tokenの有効期限が1時間で、期限切れ後の自動リフレッシュ実装が思ったより複雑だった。最終的にlocalStorageにトークンを保存して、APIエラー時に再認証フローを走らせる方式で落ち着いた。

extendedPropertiesの容量制限も注意点だ。プライベートプロパティのサイズには上限があるので、大量のデータを詰め込もうとするとエラーになる。タスクの詳細テキストはdescriptionフィールドに入れるのが正解。

ハマりポイント集

CORS問題: Google Calendar APIはブラウザから直接叩けるが、承認済みオリジンの設定ミスで詰まりやすい。Cloud Consoleの設定を確認すること。

singleEvents: trueを忘れる: 繰り返しイベントはsingleEvents: trueをつけないと正しく展開されない。

タイムゾーン: timeMin/timeMaxのパラメータはISO 8601形式(Zサフィックス付き)でないと日本時間とズレる。

まとめ

React + Google Calendar APIの組み合わせは、個人開発のタスク管理アプリとして十分実用的だ。

特に「すでにGoogleカレンダーを使っている人」にとっては、データを一元管理できる点が大きなメリットになる。OAuthやAPIの初期設定は多少面倒だが、一度動き始めれば安定している。

カスタムビューや独自フィルタリングを追加しやすいReactのコンポーネント設計と組み合わせれば、市販ツールでは実現できない自分専用の管理体験を作れる。