React(TypeScript)とFirebaseを使用して、実際に動作する施設予約システムを開発した際の実装ポイント、つまずきやすい箇所、そして工夫した点を詳しく解説します。
1. 開発した機能一覧
ユーザー認証
Firebase Authenticationによる登録・ログイン機能
予約管理
施設の予約作成・一覧表示・キャンセル機能
重複チェック
同じ日時・施設の二重予約を防止
カレンダー連携
Googleカレンダーへの自動追加機能
データ永続化
Cloud Firestoreによるリアルタイムデータ管理
レスポンシブ対応
モバイル・タブレット・PCで最適表示
使用技術スタック
- フロントエンド:React 18 + TypeScript
- ビルドツール:Vite
- ルーティング:React Router v6
- バックエンド:Firebase (Authentication, Firestore)
- ホスティング:Firebase Hosting
- スタイリング:CSS3(カスタムプロパティ使用)
2. 実装のポイント
2-1. Firebase設定の初期化
Firebaseを使用する際、最初に設定ファイルを適切に初期化することが重要です。
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
apiKey: process.env.VITE_FIREBASE_API_KEY,
authDomain: process.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: process.env.VITE_FIREBASE_PROJECT_ID,
// ... その他の設定
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);💡 工夫したポイント
環境変数を使用することで、開発環境と本番環境で異なるFirebaseプロジェクトを使い分けられるようにしました。これにより、テストデータと本番データを分離できます。
2-2. 認証状態の管理
Reactアプリケーション全体で認証状態を管理するため、onAuthStateChangedを使用しました。
function App() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
setLoading(false);
});
return () => unsubscribe();
}, []);
if (loading) {
return <div className="loading">読み込み中...</div>;
}
return (
<Router>
<Routes>
<Route path="/home" element={user ? <Home /> : <Navigate to="/login" />} />
{/* ... その他のルート */}
</Routes>
</Router>
);
}⚠️ よくある間違い
loadingステートを設けずに実装すると、初回レンダリング時にuserがnullのため、ログイン済みでも強制的にログインページにリダイレクトされてしまいます。必ずloading状態を管理しましょう。
2-3. Firestoreのデータ構造設計
予約システムでは、以下のようなデータ構造を採用しました。
// コレクション構造
group(コレクション)
└─ {userId}(ドキュメント)
├─ gname: "グループ名"
├─ mail: "メールアドレス"
└─ reserve(サブコレクション)
└─ {yyyyMMdd施設コード}(ドキュメント)
├─ date: "20260214"
└─ facilityName: "1F午前"
reserve(コレクション)
└─ {yyyyMMdd施設コード}(ドキュメント)
├─ uid: "ユーザーID"
└─ gname: "グループ名"💡 工夫したポイント
予約IDを「日付 + 施設コード」の組み合わせにすることで、重複チェックが容易になりました。また、groupコレクション配下にreserveサブコレクションを持たせることで、ユーザーごとの予約一覧を効率的に取得できます。
2-4. 予約の重複チェック
同じ日時・施設での二重予約を防ぐため、予約作成前にFirestoreでチェックを行います。
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formattedDate = formatDate(date); // "20260214"
const reserveId = `${formattedDate}${facilityCode}`;
// 既存の予約をチェック
const reserveRef = doc(db, 'reserve', reserveId);
const reserveSnap = await getDoc(reserveRef);
if (reserveSnap.exists()) {
setError('この日付・施設は既に予約されています');
return;
}
// 予約を保存
await setDoc(reserveRef, {
uid: group.uid,
gname: group.gname,
});
setSuccess('予約が完了しました!');
};⚠️ よくある間違い
非同期処理の順序を間違えると、複数のユーザーが同時に予約した際に重複が発生します。必ず「チェック → 保存」の順序を守り、トランザクションの使用も検討しましょう。
3. セキュリティルールの設定
Firestoreのセキュリティルールは非常に重要です。適切に設定しないと、データが不正にアクセスされる可能性があります。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// グループコレクション:本人のみアクセス可
match /group/{userId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
match /reserve/{reserveId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
}
// 予約コレクション:ログイン済みユーザーは読み取り可
match /reserve/{reserveId} {
allow read: if request.auth != null;
allow create: if request.auth != null;
allow delete: if request.auth != null
&& resource.data.uid == request.auth.uid;
}
}
}💡 工夫したポイント
予約の削除は予約を作成した本人のみが実行できるよう、resource.data.uidをチェックしています。これにより、他人の予約を削除できないようになっています。
4. つまずきやすいポイント
4-1. Firebase Authenticationのエラーハンドリング
Firebase Authenticationは、エラーコードが英語で返ってくるため、日本語メッセージに変換する必要があります。
try {
await signInWithEmailAndPassword(auth, email, password);
} catch (err: any) {
if (err.code === 'auth/invalid-credential') {
setError('メールアドレスまたはパスワードが正しくありません');
} else if (err.code === 'auth/email-already-in-use') {
setError('このメールアドレスは既に登録されています');
} else {
setError('ログインに失敗しました: ' + err.message);
}
}4-2. 日付フォーマットの統一
HTML5のdate inputは「YYYY-MM-DD」形式ですが、Firestoreには「YYYYMMDD」形式で保存する必要があったため、変換処理を実装しました。
const formatDate = (dateString: string) => {
return dateString.replace(/-/g, ''); // "2026-02-14" → "20260214"
};
const formatDisplayDate = (dateStr: string) => {
const year = dateStr.substring(0, 4);
const month = dateStr.substring(4, 6);
const day = dateStr.substring(6, 8);
return `${year}/${month}/${day}`; // "20260214" → "2026/02/14"
};4-3. 過去の予約を除外する処理
予約一覧では、過去の予約を表示しないようにフィルタリングが必要です。
const today = new Date();
today.setHours(0, 0, 0, 0);
const reserveList: Reservation[] = [];
querySnapshot.forEach((doc) => {
const data = doc.data();
const reserveDate = new Date(
parseInt(data.date.substring(0, 4)),
parseInt(data.date.substring(4, 6)) - 1,
parseInt(data.date.substring(6, 8))
);
// 未来の予約のみリストに追加
if (reserveDate >= today) {
reserveList.push({...data});
}
});⚠️ よくある間違い
月は0始まり(0=1月、11=12月)なので、`parseInt(data.date.substring(4, 6)) – 1`とする必要があります。これを忘れると日付が1ヶ月ずれます。
5. パフォーマンス最適化
5-1. useEffectの依存配列
不要な再レンダリングを防ぐため、useEffectの依存配列を適切に設定しました。
useEffect(() => {
const fetchGroup = async () => {
if (auth.currentUser) {
const docRef = doc(db, 'group', auth.currentUser.uid);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
setGroup({ uid: auth.currentUser.uid, ...docSnap.data() } as Group);
}
}
};
fetchGroup();
}, []); // 空配列 = 初回レンダリング時のみ実行5-2. レスポンシブデザイン
モバイルファーストで設計し、メディアクエリを使用してタブレット・PC向けのレイアウトを調整しました。
@media (max-width: 768px) {
.container {
padding: 10px;
}
.navigation {
flex-direction: column;
}
.navigation button {
width: 100%;
}
.reservation-table {
font-size: 0.9rem;
}
}6. まとめ
React + Firebase を使用した施設予約システムの実装について、以下のポイントを解説しました:
- Firebase Authentication による認証機能の実装
- Cloud Firestoreのデータ構造設計とセキュリティルール
- 予約の重複チェックロジック
- 日付フォーマットの変換処理
- エラーハンドリングとユーザーフレンドリーなメッセージ表示
- レスポンシブデザインによるモバイル対応
予約システムに限らず、React + Firebaseを使用したWebアプリケーション開発全般に応用できます。同様のシステム開発や、既存システムの改善についてのご相談は、お問い合わせフォームよりお気軽にご連絡ください。
