🕌

Spring Boot × React で作るイベント予約管理システム(フロントエンド編)

に公開

はじめに

これまでSpring Bootを使って、バックエンド側の構築を中心に学習してきました。
今回はその続きとして、Reactを用いたフロントエンド開発に取り組んだ内容をまとめます。

バックエンドで作成したAPIとReactを連携させながら、画面上でのイベント登録や予約登録、一覧表示ができるシンプルな管理画面を構築していきます。

これまでの経緯や学習過程については、以下の記事でまとめています。
まだ読んでいない方は、併せてご覧いただけると今回の内容がより理解しやすくなるかと思います。

本記事では、Reactを使った開発手順を段階的に紹介しながら、バックエンドとフロントエンドの接続までを扱います。
Reactに初めて触れる方や、Spring Bootとの連携に興味のある方の参考になれば嬉しいです。

今回の実装内容

今回は、Reactを使って以下の機能を持つイベント予約・管理画面を実装しました。

  • ユーザーの登録(POST)
  • イベントの登録(POST)
  • イベントの一覧表示(GET)
  • ユーザー・イベントを指定した予約登録(POST)
  • 登録された予約の一覧表示(GET)

これらの機能はすべて、Spring Boot側で構築したREST APIと通信しながら動作するようになっています。
ReactのフロントエンドからAPIを呼び出すことで、ユーザーが画面上でイベントや予約を操作できる仕組みです。

構成の全体像

バックエンド(Spring Boot)

  • REST APIを提供(Event, User, Reservationに関する登録・取得処理)
  • 開発時のデータベースは H2 Database を使用
    → データの永続化よりも開発スピードやテストのしやすさを優先したため、組み込みDBであるH2を選択しました。

フロントエンド(React)

  • イベントや予約のフォーム・一覧画面を作成
  • APIと連携して状態管理(useState, useEffect)を行い、画面に反映

開発環境と本番環境の違い

開発中は、React(フロントエンド)とSpring Boot(バックエンド)を分けて起動して作業を進めています。
それぞれ別の開発サーバーで動かし、APIを通じてデータをやりとりする形です。

このように分けている理由は以下の通りです:

  • Reactの開発サーバー(npm start)にはホットリロード機能があり、変更をすぐに画面に反映できるため、効率よく開発できる
  • フロントとバックを独立させることで、それぞれの動作確認やテストがしやすくなる
  • 開発の途中でも、役割ごとに整理された構成で進められる

一方で、本番環境ではフロントとバックをまとめて動かす構成にします。

具体的には:

  • Reactでビルドした静的ファイル(npm run build で生成)を、
  • Spring Bootアプリの resources/static フォルダに配置し、
  • ひとつのアプリケーションとしてデプロイします。

このように、

  • 開発中は柔軟に作業できるように分離構成で進めて、
  • 本番環境ではまとめて運用しやすい構成にすることで、
    それぞれのメリットを活かした開発ができるように工夫しています。

IntelliJ IDEAでReact開発環境を構築する

Reactの開発を始めるために、まずはIntelliJ IDEA上で環境準備を行いました。
今回は以下の手順でセットアップしました。

1. Node.jsのインストール

Reactを動かすには Node.js が必要です。
以下の手順でインストールしました:

  1. Node.js公式サイト にアクセス
  2. LTS(推奨版)」の Windows インストーラー(.msi)をダウンロード
  3. インストーラーを実行し、画面の指示に従って進める

ポイント:

  • 基本的に デフォルト設定のままでOK
  • Add to PATH」にチェックが入っていることを確認してインストールしてください

2. フォルダ構成について

今回のプロジェクトは、フロントエンドとバックエンドを分けて管理しています。
フォルダ構成は以下のようにしています:

event-reservation-app/
├── backend/           ← Spring Bootのソースを管理
│   ├── src/
│   ├── pom.xml
│   └── ...
├── frontend/          ← Reactアプリのフォルダ
│   ├── src/
│   └── package.json
├── docker-compose.yml ← 最終的に追加予定のDocker設定ファイル
└── README.md          ← プロジェクト全体の概要

3. Reactアプリの作成

frontend フォルダ内で React のひな型を作成しました。
以下のコマンドを使用します:

npx create-react-app frontend

これで、frontend/ フォルダ内に React の基本的な構成ファイルが生成されます。

4. React開発サーバーの起動

作成した React アプリを開発用に起動します

cd frontend
npm start

ブラウザが自動で開いて、React の初期画面(ロゴ画面)が表示されれば成功です。
この開発サーバーはホットリロードに対応しているため、ソースコードを編集するとすぐに画面に反映されます。

イベント登録画面(POST送信)

Reactでイベント登録フォームを作り、入力した内容をSpring BootのAPIにPOST送信する流れを紹介します。
ユーザー登録画面もほぼ同じ仕組みで作っています。

import React, { useState } from 'react';
import axios from 'axios';
import dayjs from 'dayjs';

const EventForm = ({ onEventAdded }) => {
  const [title, setTitle]     = useState('');
  const [date, setDate]       = useState('');  // datetime-local用
  const [message, setMessage] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const isoDate = dayjs(date).toISOString();
      await axios.post('/events', { title, date: isoDate });
      setMessage('イベントを登録しました!');
      setTitle('');
      setDate('');

      if (onEventAdded) {
        onEventAdded();  // 一覧を更新してもらう
      }
    } catch (err) {
      console.error(err);
      setMessage('登録に失敗しました…');
    }
  };

  return (
    <form
      onSubmit={handleSubmit}
      style={{
        backgroundColor: '#fff',
        padding: '24px',
        borderRadius: '8px',
        boxShadow: '0 2px 6px rgba(0, 0, 0, 0.08)',
        fontFamily: 'Arial, sans-serif',
        maxWidth: '500px',
        margin: '32px auto',
        border: '1px solid #ddd',
      }}
    >
      <h2 style={{
        color: '#900',
        marginBottom: '20px',
        borderBottom: '2px solid #ccc',
        paddingBottom: '8px'
      }}>
        ✍ イベント登録
      </h2>

      <div style={{ marginBottom: '16px' }}>
        <label style={{ display: 'block', marginBottom: '6px', color: '#333' }}>
          タイトル:
        </label>
        <input
          type="text"
          value={title}
          onChange={e => setTitle(e.target.value)}
          required
          style={{
            width: '100%',
            padding: '8px',
            borderRadius: '4px',
            border: '1px solid #ccc',
            fontSize: '14px',
          }}
        />
      </div>

      <div style={{ marginBottom: '16px' }}>
        <label style={{ display: 'block', marginBottom: '6px', color: '#333' }}>
          日時:
        </label>
        <input
          type="datetime-local"
          value={date}
          onChange={e => setDate(e.target.value)}
          required
          style={{
            width: '100%',
            padding: '8px',
            borderRadius: '4px',
            border: '1px solid #ccc',
            fontSize: '14px',
          }}
        />
      </div>

      <button
        type="submit"
        style={{
          backgroundColor: '#900',
          color: 'white',
          padding: '10px 20px',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
          fontWeight: 'bold',
        }}
      >
        登録する
      </button>

      {message && (
        <p style={{
          marginTop: '16px',
          color: message.includes('失敗') ? 'red' : 'green'
        }}>
          {message}
        </p>
      )}
    </form>
  );
};

export default EventForm;

ポイント解説

  • useStateで入力値を管理
    タイトルと日時のフォームの値を状態として管理しています。

  • 日時はISO形式に変換して送信
    入力は datetime-local 形式ですが、API送信時には dayjs で ISO8601 形式に変換しています。

  • axiosでPOSTリクエスト送信
    axios.post でバックエンドの /events エンドポイントにデータを送っています。

  • 成功・失敗時のメッセージ表示
    登録成功でフォームをリセットし、メッセージを表示。失敗時もエラーメッセージを出します。

  • 親コンポーネントへの通知
    登録成功後に onEventAdded コールバックを呼んで、イベント一覧の更新を促せます。

イベント一覧表示画面(GET取得)

import React from 'react';
import dayjs from 'dayjs';

const EventList = ({ events = [] }) => {
  return (
    <div>
      <h2 style={{ color: '#900', borderBottom: '2px solid #ccc', paddingBottom: '8px' }}>
        🎪 イベント一覧
      </h2>
      {events.length === 0 ? (
        <p>イベントがまだ登録されていません。</p>
      ) : (
        events.map(event => (
          <div key={event.id} style={{ /* カードスタイル */ }}>
            <h3 style={{ margin: 0, color: '#333' }}>{event.title}</h3>
            <p style={{ margin: '4px 0 0', color: '#555' }}>
              {dayjs(event.date).format('YYYY年MM月DD日 HH:mm')}
            </p>
          </div>
        ))
      )}
    </div>
  );
};

export default EventList;

ポイント解説

  • events は親コンポーネントから渡されるイベントの配列
    デフォルトで空配列 ([]) にしています。
  • イベント未登録時の表示
    events.length === 0 の場合は「イベントがまだ登録されていません。」と案内します。
  • map関数でのリスト表示
    events.map(...) で各イベントを描画し、タイトルと日時を表示します。
  • 日時の整形
    dayjs(event.date).format('YYYY年MM月DD日 HH:mm') で見やすい形式にしています。
  • カード風デザイン
    シンプルで余白や境界線を持たせたスタイルにし、一覧が読みやすくなるよう工夫しています。
  • 更新タイミング
    イベント登録フォーム(EventForm)と連携して、登録完了後に一覧を再取得・再描画する流れです。
  • ユーザー一覧リストも同様の仕組み
    ユーザー登録リストの画面もほぼ同じ構造・流れで実装しています。

予約登録フォーム(ユーザーID・イベントID指定 → POST送信)

約登録フォームは、ユーザーとイベントを選択して予約情報をサーバーに送る機能です。
今回の実装では、ReactのuseStateで選択状態を管理し、axiosを使ってAPIにPOST送信しています。

import React, { useState } from 'react';
import axios from 'axios';
import dayjs from 'dayjs';

const ReservationForm = ({ users = [], events = [], onReservationAdded }) => {
  const [userId, setUserId]     = useState('');
  const [eventId, setEventId]   = useState('');
  const [message, setMessage]   = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await axios.post('/reservations', {
        userId: Number(userId),
        eventId: Number(eventId)
      });
      setMessage('✅ 予約を登録しました!');
      setUserId('');
      setEventId('');
      onReservationAdded && onReservationAdded();
    } catch (err) {
      console.error(err);
      setMessage('⚠️ 予約登録に失敗しました…');
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{
      backgroundColor: '#fff',
      padding: '24px',
      borderRadius: '8px',
      boxShadow: '0 2px 6px rgba(0,0,0,0.08)',
      maxWidth: '500px',
      margin: '32px auto',
      border: '1px solid #ddd',
      fontFamily: 'Arial, sans-serif'
    }}>
      <h2 style={{
        color: '#900',
        borderBottom: '2px solid #ccc',
        paddingBottom: '8px',
        marginBottom: '16px'
      }}>
        📝 予約登録
      </h2>

      {/* ユーザー選択 */}
      <div style={{ marginBottom: '16px' }}>
        <label style={{ display: 'block', marginBottom: '6px', color: '#333' }}>
          ユーザー:
        </label>
        <select
          value={userId}
          onChange={e => setUserId(e.target.value)}
          required
          style={{
            width: '100%', padding: '8px', borderRadius: '4px',
            border: '1px solid #ccc', fontSize: '14px'
          }}
        >
          <option value="" disabled>ユーザーを選択してください</option>
          {users.map(user => (
            <option key={user.id} value={user.id}>
              {user.name} ({user.email})
            </option>
          ))}
        </select>
      </div>

      {/* イベント選択 */}
      <div style={{ marginBottom: '16px' }}>
        <label style={{ display: 'block', marginBottom: '6px', color: '#333' }}>
          イベント:
        </label>
        <select
          value={eventId}
          onChange={e => setEventId(e.target.value)}
          required
          style={{
            width: '100%', padding: '8px', borderRadius: '4px',
            border: '1px solid #ccc', fontSize: '14px'
          }}
        >
          <option value="" disabled>イベントを選択してください</option>
          {events.map(ev => (
            <option key={ev.id} value={ev.id}>
              {ev.title} ({dayjs(ev.date).format('YYYY/MM/DD HH:mm')})
            </option>
          ))}
        </select>
      </div>

      <button type="submit" style={{
        backgroundColor: '#900', color: 'white',
        padding: '10px 20px', border: 'none',
        borderRadius: '4px', cursor: 'pointer',
        fontWeight: 'bold'
      }}>
        予約する
      </button>

      {message && (
        <p style={{
          marginTop: '16px',
          color: message.includes('失敗') ? 'red' : 'green'
        }}>
          {message}
        </p>
      )}
    </form>
  );
};

export default ReservationForm;

ポイント解説

  • userId と eventId は useState で管理
    選択された値をそれぞれ状態として保持します。
  • 予約APIへの送信
    フォーム送信時に axios.postで/reservationsに にデータを送信。
    数値型に変換しているのはサーバー側の型に合わせるためです。
  • 成功時の処理
    予約が成功したらフォームの入力をリセットし、成功メッセージを表示します。
  • 一覧更新の通知
    onReservationAdded コールバックを呼び出すことで、親コンポーネントに予約追加を通知し、一覧を再取得できます。
  • 選択UIの工夫
    <select> タグでユーザー・イベントを選択し、最初に disabled の空選択肢を設けることで、未選択状態を明示しています。

App.js で全体の動作確認

Reactアプリの起点となる App.js に、これまで作成した各コンポーネントを組み込んで、画面全体の動作確認を行いました。

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import UserForm from './components/UserForm';
import EventForm from './components/EventForm';
import EventList from './components/EventList';
import ReservationForm from './components/ReservationForm';
import ReservationList from './components/ReservationList';

function App() {
  const [users, setUsers]             = useState([]);
  const [events, setEvents] = useState([]);
  const [reservations, setReservations] = useState([]);

  const fetchUsers = async () => {
    const res = await axios.get('/users');
    setUsers(res.data);
  };
  const fetchEvents = async () => {
    const res = await axios.get('/events');
    setEvents(res.data);
  };
  const fetchReservations = async () => {
    const res = await axios.get('/reservations');
    setReservations(res.data);
  };

  useEffect(() => {
    fetchUsers();
    fetchEvents();
    fetchReservations();
  }, []);

  const containerStyle = {
    maxWidth: '500px',
    margin: '0 auto',
    padding: '16px'
  };

  return (
    <div style={{ padding: '32px 0', backgroundColor: '#f9f9f9' }}>
      <div style={containerStyle}>
        <UserForm onUserAdded={fetchUsers} />
      </div>
      <div style={containerStyle}>
        <EventForm onEventAdded={fetchEvents} />
      </div>
      <div style={containerStyle}>
        <EventList events={events} />
      </div>
      <div style={containerStyle}>
        <ReservationForm  users={users} events={events}  onReservationAdded={fetchReservations} />
      </div>
      <div style={containerStyle}>
        <ReservationList reservations={reservations} />
      </div>
    </div>
  );
}

export default App;

ポイント解説

  • 初回読み込み時のデータ取得
    useEffect(() => { fetchUsers(); fetchEvents(); fetchReservations(); }, []);
    → アプリ起動時にユーザー・イベント・予約データを取得し、初期表示を行います。
  • 登録後の最新データ取得
    各フォームコンポーネントに on〜Added 関数を渡すことで、登録後に最新データを再取得できます。
    <UserForm onUserAdded={fetchUsers} />
    
  • 一画面での統合表示
    ユーザー登録、イベント登録、予約登録、一覧表示を一つの画面にまとめることで、
    「ユーザー登録 → イベント登録 → 予約登録 → 一覧確認」までを流れるように操作できる構成になっています。

今回はスキップする開発部分

今回の画面構成は、開発初期の機能確認用の統合ビューとして構築しましたが、
今後は以下のような改善を予定しています。

今後の改善ポイント

  • 画面を役割ごとに分割予定
    例:ユーザー管理画面/イベント管理画面/予約一覧画面 など
    → React Router を使ってページを切り替えられるようにする

  • ログイン済みアカウントごとの表示制御を実装予定

    • 管理者だけがイベント登録できる
    • 一般ユーザーは予約のみ可能

おわりに

今回は、Reactを使って以下のような簡単なイベント予約システムのフロントエンドを構築してきました。

  • ユーザー登録フォーム
  • イベント登録フォーム
  • イベント一覧表示
  • 予約登録フォーム(ユーザー・イベント選択式)
  • 予約一覧表示
  • App.js で統合&動作確認

まだ開発途中ではありますが、Spring Bootと連携してフルスタックな開発ができた実感があり、とても学びが多かったです。


今後のステップ(予定)

以下のような順で、本番構成やクラウド環境にも挑戦していきます。

  1. React と Spring Boot の統合(本番構成へ)

    • npm run build で React アプリをビルド
    • ビルド成果物を Spring Boot の resources/static に配置し、バックエンドと統合
  2. Docker Compose によるローカル環境構築

    • Spring Boot + React + PostgreSQLdocker-compose.yml で一体化
    • docker-compose up 一発でアプリ全体が立ち上がるように整備
  3. AWS デプロイ(EC2 または ECS を予定)

    • Docker 化したアプリケーションをクラウドへデプロイ
    • インターネットからアクセスできる予約システムとして公開予定

Discussion