FirebaseとReactで作るお問合せ管理

記事
IT・テクノロジー

FirebaseとReactで作るお問合せ管理

Vueでの事例を紹介しましたが同じアプリをReactで!
前回はフロントエンドのフレームワークとしてVueを使ってFirebaseの機能を取り込んだWebサービス・Webアプリの事例を紹介させて頂きました。
今回は、Vueとならんで広く利用されているフレームワークReactを使った事例を同じお問合せフォームの管理アプリで紹介します。

FirebaseとReactを組み合わせて使う場合、Vueの場合と同様に「npm」の活用が便利です。Firebaseのモジュールもインストールしてしまえば、簡単にReactのプロジェクトのコードから呼び出す事ができます。

Reactを使うための準備

「create-react-app」のインストール
Vueの場合Vue CLIを使いましたがReactの場合は、「create-react-app」を使います。このパッケージを使うとReactのサンプルプロジェクトを作成してくれます。VueのようなUIはサポートされていませんが実用上は全く問題ありません。
「create-react-app」のインストールは以下のコマンドで行います。
$ npm install -g create-react-app

Reactのプロジェクトの作成

create-react-app」のインストールが済んだら、まずReactのプロジェクトを作ります。VueのようなUIはありませんがコマンドを一つ実行するだけです。
$ npx create-react-app [プロジェクトの名前] --template typescript

今回も基本はTypescriptで実装する事にするので、最後のオプションを付けます。「--template」のオプションをつけるとTypescriptで開発する環境を含めて設定してくれます
Javascriptだけで開発する場合はこのオプションは不要です。

テンプレートのプロジェクトを実行してみる
Reactもサンプルのプロジェクトが作成されているのでまずは、それを動かしてみます。
開発用のサーバーを起動するには作成したプロジェクトのフォルダーに移動して以下のコマンドをタイプして実行するだけです。

$ npm start

ここで表示されているのが作成されたサンプルのプロジェクトのアプリの画面です。
プロジェクトのフォルダーの構成は、以下の様になっています。Vueと似たような構成です。
(ご使用になるパッケージのバージョンや設定で多少異なる場合があります)

public
   favicon.ico
   index.html
   log192.png
   log512.png
   manifest.json
   robots.txt
src
   App.css
   App.test.tsx
   App.tsx
   index.css
   indtx.tsx
   logo.svg
   react-app-env..d.ts
   serviceWorker.ts
   setupTests.ts
.gitignore
package-lock.json
package.json
README.md
tsconfig.json

お問合せフォームの完了にプロジェクトのファイルを変更する

Firebaseコンソールでやる事は前回のVueを利用する場合と全く同じなのでここでは省略します。詳しくは前回の記事をご覧ください。
App.tsxを変更します。。 
個人的に「class形式のオブジェクト」の方が好みなので書き換えています。
*関数形式からクラス形式に変更
*「logo」のインポートを削除
*トップレベルの「div」の中身を「<h1>ホーム</h1>」に置き換え
うになっています。まずは不要な部分を削除します。
変更後のコードはこのようになります。先ほどのテスト用のサーバーを開けた状態の場合、変更をした後にファイルをセーブされると先ほどのReactのロゴと表示はなくなって「ホーム」が表示されるはずです。
import { render } from "@testing-library/react";
import React from "react";
import "./App.css";
class App extends React.Component {
  render() {
    return (
      <div className="App">
        <h1>ホーム</h1>
      </div>
    );
  }
}
export default App;

不要になったファイルを削除します
変更で不要になったファイルを削除します。
*App.text.tsx
*log.svg
*serviceWorker.ts
*setupTest.ts
*public/log192.png
*public/log512.png

public/manifest.jsonはファイルを削除したため書き換えます。これは、「public」フォルダにあった*log192.png*と*log512.png*を削除したので、それに関連したエントリを除くためです。
{
  "short_name": "React App",
  "name": "Create React App Sample",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

React側での設定

Firebaseのモジュールをインストールします。以下のコマンドをプロジェクトのトップフォルダで実行します。
$ npm install firebase

インストールしたら、プロジェクトのトップフォルダに「lib」というフォルダーを作って、「firebase.ts」を作成します。(Javascriptを使用する場合は、firebase.js)必要な情報は、Firebaseのコンソールから取得した情報をコピーします。
以下がその中身の例です。
(個別のプロジェクト情報に置き換える必要があります。)
フォルダーの名前や場所、ファイルの名前は何でも良いのですが、今回は私が普段設定する名前を例に挙げています。

import firebase from "firebase";
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
    apiKey: "API KEYの情報",
    authDomain: "ユーザー認証のドメイン",
    databaseURL: "データベースのURL",
    projectId: "プロジェクトID",
    storageBucket: "ストレージのバケット",
    messagingSenderId: "メッセージ送信のID",
    appId: "アプリのID",
    measurementId: "アナリティクスで使用するID"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.analytics();
export default firebase;

App.tsxを変更して管理画面をつくります!

Typescriptのインターフェース定義と定数の定義を一か所にまとめます
見やすいソースコードにするために、Typescriptで使うインターフェースの定義と定数の定義を1か所にまとめます。こうする事で、コード中の「ハードコーディング(直接値をプログラムで使う)」を最小限にする事ができます。
さらに、変更が必要な場合も一か所直せば修正ができるので開発の効率向上と管理しやすいプログラムにすることができます。今回は「lib」フォルダーの下に。
* constants.ts 
* types.ts
この部分は前回のVueの場合と同じです。
constant.tsの中身です。
export const QUERY_COLLECTION = "query";

types.tsの中身です
export interface Query {
  id: string;
  email: string;
  message: string;
  name: string;
  subject: string;
}

これはお問合せ内容のデータの構造を定義した物です。
これぐらいの規模のアプリでは余り問題になりませんが、大きなアプリになると差が出てきます。
ReactでもBootstrapを活用
Vueの場合と同様にBootstrapを活用します。「public」フォルダの下のinex.htmlにBootstrapのCSSファイルの読み込みを追加します。

src/index.tsxも変更しておきます

* serviceWorkerのインポートを削除
*最後の部分のserviceWorker.unregister();も削除します。
変更後のファイルは以下のようになります。
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);
App.tsxの全容

今回はシンプルに全ての記述を一つのファイル(App.tsx)に書いてしまいます。
基本的に処理内容はVueで作成した場合と同じですが、Reactの場合は、JSXを採用しているためにVueとはHTMLの部分の記述の部分が違います。記述が、Typescript(Javascript)の中にかかれるために、Typescript/Javascriptの予約語になっている「class」「for」などは使えません。それぞれ、「className」「htmlFor」などに書き換える必要があります。
VueはHTMLにプログラム的要素を取り入れて、HTMLの中で、繰り返しになる要素を展開していますが、Reactの場合は、少しやり方が違っていて、JSXの要素を「map」などの関数を使って生成しています。
また、条件付きで表示する部分も、Typescript/Javascriptの条件でレンダリングの返り値を変えるように記述します。
最初にApp.tsxファイルの全容です。

import React from "react";
import firebase from "./lib/firebase";
import { produce } from "immer";
import * as CONSTANT from "./lib/constants";
import * as TYPE from "./lib/types";
import "./App.css";
interface IState {
  queryList: Array<TYPE.Query>;
  selectedQuery: TYPE.Query | undefined;
  selected: boolean;
}
interface IProps {}
class App extends React.Component<IProps, IState> {
  // HTMLのInput DOMへのリファレンス
  private email: React.RefObject<HTMLInputElement> = React.createRef<
    HTMLInputElement
  >();
  private password: React.RefObject<HTMLInputElement> = React.createRef<
    HTMLInputElement
  >();
  // コンストラクタ(初期化はここで行います)
  constructor(props: any) {
    super(props);
    // ステートの初期化
    this.state = {
      queryList: [],
      selectedQuery: undefined,
      selected: false,
    };
  }
  // 個別のお問合せからお問合せのリスト表示に戻るための処理
  backEvent():void {
    this.setState({
      selectedQuery: undefined,
      selected: false,
    });
  }
  // 選択されたお問合せ内容を削除するための処理
  deleteEvent(e: React.MouseEvent<HTMLButtonElement, MouseEvent>):void {
    const id = e.currentTarget.id;
    const selected: TYPE.Query | undefined = this.state.queryList.find(
      (item: TYPE.Query): boolean => {
        return item.id === id;
      }
    );
    if (selected) {
      firebase
        .firestore()
        .collection(CONSTANT.QUERY_COLLECTION)
        .doc(selected.id)
        .delete()
        .then(() => {
          this.getQuery();
        })
        .catch((error) => {
          alert("選択したお問合せを削除できませんでした!");
        });
    }
  }
  // Firebase Cloud Firestore からお問合せのリストを取得
  getQuery():void {
    this.setState({
      queryList: [],
    });
    firebase
      .firestore()
      .collection(CONSTANT.QUERY_COLLECTION)
      .get()
      .then(
        (
          querySnapshot: firebase.firestore.QuerySnapshot<
            firebase.firestore.DocumentData
          >
        ) => {
          querySnapshot.forEach((doc: firebase.firestore.DocumentData) => {
            const query: TYPE.Query = {
              id: doc.id,
              email: doc.data().email,
              message: doc.data().message,
              name: doc.data().name,
              subject: doc.data().subject,
            };
            const updateList = produce(this.state.queryList, (draft) => {
              draft.push(query);
            });
            this.setState({
              queryList: updateList,
            });
          });
        }
      );
  }
  // ログイン状態の確認
  isLogin(): boolean {
    if (firebase.auth().currentUser) {
      return true;
    } else {
      return false;
    }
  }
  // ログイン処理
  loginEvent():void {
    if (this.email.current && this.password.current) {
      firebase
        .auth()
        .signInWithEmailAndPassword(
          this.email.current.value,
          this.password.current.value
        )
        .then((user) => {
          if (user) {
            // Login is successful
            this.getQuery();
          } else {
            // Login is failed
            this.setState({
              queryList: [],
            });
          }
        })
        .catch((err) => {
          // Login failed
          this.setState({
            queryList: [],
          });
        });
    }
  }
  // 個別のお問合せ内容を選択する処理
  selectEvent(e: React.MouseEvent<HTMLTableCellElement, MouseEvent>):void {
    const id = e.currentTarget.id;
    const selected: TYPE.Query | undefined = this.state.queryList.find(
      (item: TYPE.Query): boolean => {
        return item.id === id;
      }
    );
    if (selected) {
      this.setState({
        selectedQuery: selected,
        selected: true,
      });
    }
  }
  render() {
    const loginForm: JSX.Element = (
      <div className="login_form">
        <form>
          <div className="form-group">
            <label htmlFor="exampleInputEmail1">Email address</label>
            <input
              type="email"
              className="form-control"
              id="exampleInputEmail1"
              aria-describedby="emailHelp"
              ref={this.email}
            />
          </div>
          <div className="form-group">
            <label htmlFor="exampleInputPassword1">Password</label>
            <input
              type="password"
              className="form-control"
              id="exampleInputPassword1"
              ref={this.password}
            />
          </div>
          <button
            type="button"
            className="btn btn-primary"
            onClick={() => this.loginEvent()}
          >
            Submit
          </button>
        </form>
      </div>
    );
    const querlyTableBody: Array<JSX.Element> = this.state.queryList.map(
      (query) => (
        <tr key={query.id}>
          <td
            className="selectable_cell"
            id={query.id}
            onClick={(e: React.MouseEvent<HTMLTableCellElement, MouseEvent>) =>
              this.selectEvent(e)
            }
          >
            {query.email}
          </td>
          <td>{query.name}</td>
          <td>{query.subject}</td>
          <td>{query.message}</td>
          <td>
            <button
              className="btn btn-danger"
              id={query.id}
              onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) =>
                this.deleteEvent(e)
              }
            >
              Delete
            </button>
          </td>
        </tr>
      )
    );
    const noQuery: JSX.Element = (
      <div className="no_query">
        <h2>お問合せはありません</h2>
      </div>
    );
    const queryData:JSX.Element = (
      <div className="selected_query">
        <button className="btn btn-success" onClick={() => this.backEvent()}>
          戻る
        </button>
        <dl>
          <dt>Email</dt>
          <dd>
            {this.state.selectedQuery ? this.state.selectedQuery.email : ""}
          </dd>
          <dt>名前</dt>
          <dd>
            {this.state.selectedQuery ? this.state.selectedQuery.name : ""}
          </dd>
          <dt>お問合せ件名</dt>
          <dd>
            {this.state.selectedQuery ? this.state.selectedQuery.subject : ""}
          </dd>
          <dt>お問合せ内容</dt>
          <dd>
            {this.state.selectedQuery ? this.state.selectedQuery.message : ""}
          </dd>
        </dl>
      </div>
    );
    if (this.isLogin()) {
      // ログインしている場合
      if (this.state.queryList.length === 0) {
        // お問合せがない場合
        return <div className="App">{noQuery}</div>;
      } else {
        // お問合せがある場合
        if (this.state.selected) {
          // お問合せが選択されている場合
          return <div className="App">{queryData}</div>;
        } else {
          // お問合せが選択されていない場合
          return (
            <div className="App">
              <div className="all_list">
                <table className="table table-sm">
                  <thead className="thead-dark">
                    <tr>
                      <th scope="col">Email</th>
                      <th scope="col">名前</th>
                      <th scope="col">お問合せ件名</th>
                      <th scope="col">お問合せ内容</th>
                      <th scope="col"></th>
                    </tr>
                  </thead>{" "}
                  <tbody>{querlyTableBody}</tbody>
                </table>
              </div>
            </div>
          );
        }
      }
    } else {
      // ログインしていない場合 -- ログインフームを表示
      return <div className="App">{loginForm}</div>;
    }
  }
}
export default App;

Firebaseのお問合せのドキュメントのIDを追加しておくと後の削除の処理が簡単になるので、Firebaseデータベースのドキュメント内のデータに加えてドキュメントIDを取り込んでいます。

CSSの設定
表示の体裁を整えるCSSの部分は今回はApp.cssにCSSの記述を追加しました。App.cssの例です
.App {
  text-align: center;
}
.App-logo {
  height: 40vmin;
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}
.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}
.App-link {
  color: #61dafb;
}
.login_form {
  text-align: left;
  background-color: lightgray;
  width: 500px;
  padding: 25px;
  border-radius: 5px;
  margin: 30px auto;
}
.login_form label {
  font-weight: bold;
}
.all_list {
  width: 90%;
  margin: 30px auto;
}
.all_list table {
  background-color: lightgray;
  border-radius: 5px;
}
.selectable_cell {
  color: black;
}
.selectable_cell:hover {
  color: dimgray;
  text-decoration: underline;
}
.selected_query {
  text-align: left;
  background-color: lightgray;
  border-radius: 5px;
  width: 600px;
  margin: 30px auto;
  padding: 25px;
}
.selected_query button {
  margin-bottom: 20px;
}
.selected_query dt {
  border-bottom: 2px solid dimgray;
  margin-top: 20px;
}
.selected_query dd {
  margin-left: 50px;
}
@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

セキュリティルールを設定して完了!
最後にセキュリティルールを設定すれば完了です!
一般利用者は「query」コレクションにドキュメントの新規作成の権限を、管理者には全てのデータの読み書きの権限を与えるルールです。
これで、お問合せ内容を見るには管理者としてログインが必要になるようになりました。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // 管理者は全てのドキュメントの読み書きができます
    // 管理者以外の人にはここでのアクセスは許可しません
    match /{document=**} {
     allow read, write: if request.auth.uid == "5X8xvRKVbddpR7oDXduQjVRb8cv1";
    }
    // 全ての人に「query」コレクションの新規作成を許可します
    // (利用者全ての人がお問合せを登録可能)
    match /query/{queryId} {
     allow create;
    }
  }

まとめ

FirebaseのデータベースをReactから使うのもVueとほぼ同じです!
Reactを利用してFirebaseに登録されたお問合せ内容を管理するアプリの例を解説してきました。中身は殆どVueで制作したものと同じです。
この規模のアプリだとReactでもVueでも殆ど違いはありません。どちらでも好みの物を使えば良いかと思います。
*Firebaseのプロジェクトの作成と設定
*create-react-appのモジュールを利用してプロジェクトの準備
*FirebaseのモジュールをReactに組み込み
*お問合せの管理機能の実装
*セキュリティールールの設定
これで、基本的な開発は完了です。
これをインターネット上で公開(ディプロイ)する方法については、別途記事を書く予定です。
他のWebアプリやWebサービスでも基本的にやる事は機能の実装以外は同じです。








サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す