FirebaseとReact+TypeScriptの連携

# FirebaseとReact+TypeScriptの連携

※同じ記事を[こちら](https://ttis.croud.jp/?uuid=9c651171-46e6-4af6-8aaf-11ee7139a688)にも書いています

今回は以下の機能を利用します

- Firebase
  - hosting    静的コンテンツの設置とfunctionsへのプロキシ
  - functions　databaseへの入出力
  - database　 データ保存
 
　Webからのアクセスはfunctionsも含め、必ずhostingを介するようにします。これで静的コンテンツとデータアクセス用の窓口のドメインが同一になるので、CORS対策で余計な処理を付け加える必要がなくなります。


　Reactに関してはトランスコンパイル後のファイルを静的コンテンツとしてhostingに配置します。フロントエンドからFirebaseのdatabaseへのアクセスはfunctions経由となるので、フロントエンド側にAPIキーの設定をする必要はありません。今回はキーを一切使わないコードとなっています。

## １．基本設定

### 1.1 Firebaseツールのインストール

Node.jsが入っていることが前提です
以下のコマンドで必要なツールをインストールします

```  
npm -g i firebase-tools
```

### 1.2 Firebase上にプロジェクトを作成
2019/10/18 17:16:45
https://console.firebase.google.com/へ行って、プロジェクトを作成します。
コマンドラインから作ることも可能ですが、IDが被った場合に対処しにくいので、Web上から作った方が簡単です。

ローカルに開発環境を作る

まずは新規ディレクトリを作って、そこをカレントディレクトリにしてください

・Firebaseにログインし、コマンドからの操作をするための権限を取得する   

```
firebase login   
```

・質問が出たらエンターキー 

```
? Allow Firebase to collect CLI usage and error reporting information? 
```

・ブラウザからユーザ認証 

・Firebaseプロジェクトの作成 

```
  firebase init 
? Are you ready to proceed? (Y/n)  <- エンターキー 
```

・Database、Functions、Hostingを選ぶ 

```
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection)  
 (*) Database: Deploy Firebase Realtime Database Rules  
 ( ) Firestore: Deploy rules and create indexes for Firestore  
 (*) Functions: Configure and deploy Cloud Functions  
 (*) Hosting: Configure and deploy Firebase Hosting sites  
 ( ) Storage: Deploy Cloud Storage security rules 
```

・プロジェクトの選択は先ほど作ったプロジェクトを選ぶ

```
? Please select an option: (Use arrow keys)
> Use an existing project <- これを選択
```

・データベースのルール

```
? What file should be used for Database Rules? database.rules.json <- エンターキー
```


・開発言語の選択

```
  TypeScript
```

・TSLintはNo

・残りは全てエンターキー


### 1.3 React環境の構築

package.jsonの作成

```
npm -y init
```

React環境構築パッケージのインストール

```
npm -D i setup-template-firebase-react
```

必要パッケージ類のインストールを確定させる

```
npm i
```

Reactのビルド(自動ビルドの場合はnpm run watch)

```
npm run build
```

インストールしたパッケージは以下のような構造を作ります

```
root/  
　├ public/ (ファイル出力先)  
　└ front/ (フロントエンド用ディレクトリ)  
　　├ public/ (リソースHTMLファイル用)  
　　│　└ index.html  
　　├ src/ (JavaScript/TypeScript用ディレクトリ)  
　　│　├ .eslintrc.json  
　　│　├ index.tsx  
　　│　└ tsconfig.json  
　　└ webpack.config.js  
```


### 1.4 firebaseエミュレータの起動

エミュレータの起動
（functionsがコンパイルされてないのでエラーを出しますが、ここでは無視してください）

```
npm start
```

確認

```
http://localhost:5000/
```

「今日は世界！」と表示されればOK

## ２．掲示板を作る

### 2.1 functionsの設定

データが送られてきたらdatabaseに書き込み、そうでない場合もdatabase上のデータを返すプログラムです
作成が終わったらfunctionsディレクトリでnpm buildを行う必要があります

```/functions/src/index.ts
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";

admin.initializeApp(functions.config().firebase);

export const proc = functions.https.onRequest((request, response) => {
  (async () => {
    try {
      const { name, body } = request.body;
      if (name && body) {
        const date = new Date().toISOString();
        await admin
          .database()
          .ref("/bbs")
          .push({ name, body, created_at: date, update_at: date });
      }
    } catch {}
  })();

  admin
    .database()
    .ref("/bbs")
    .orderByChild("created_at")
    .on("value", data => {
      const values = data!.val();
      const result = Object.entries(values).map(([key, value]) => ({
        ...value,
        id: key
      }));
      response.status(200).send(result);
    });
});
```


### 2.2 hostingの設定

functionsで作成したコードがhostingを経由できるように、rewritesの設定を追加します
これをやったら、firebaseのemulaterを再起動する必要があります

```/firebase.json
{
  "database": {
    "rules": "database.rules.json"
  },
  "functions": {
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint",
      "npm --prefix \"$RESOURCE_DIR\" run build"
    ],
    "source": "functions"
  },
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "/proc",
        "function": "proc"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
} 
```


### 2.3 フロントエンド側の作成

ここから先はReactのフロントエンドプログラムとなります

#### 2.3.1 パッケージの追加インストール

フロントエンド側で状態管理や通信を行うためのモジュールを追加します

```
npm -D i react-redux @types/react-redux @jswf/redux-module @jswf/adapter
```

#### 2.3.2 ソースコードの追加

・Storeデータ管理とFirebaseとの通信機能の実装

```/front/src/MessageModule.ts
import { ReduxModule } from "@jswf/redux-module";
import { Adapter } from "@jswf/adapter";

//メッセージの構造
interface Message {
  id: number;
  name: string;
  body: string;
  created_at: Date;
  updated_at: Date;
}
//Storeで使う構造
interface State {
  messages: Message[];
}
//データモジュールの定義
export class MessageModule extends ReduxModule<State> {
  protected static defaultState: State = {
    messages: []
  };
  public write(message: { name: string; body: string }) {
    Adapter.sendJsonAsync("proc", message).then(e => {
      if (e instanceof Array) this.setState({ messages: e as Message[] });
    });
  }
  public load() {
    Adapter.sendJsonAsync("proc").then(e => {
      if (e instanceof Array) this.setState({ messages: e as Message[] });
    });
  }
} 
```

・入力フォーム

```tsx:/front/src/MessageForm.tsx
import { useRef } from "react";
import React from "react";
import { useModule } from "@jswf/redux-module";
import { MessageModule } from "./MessageModule";

export function MessageForm() {
  const messageModule = useModule(MessageModule, undefined, true);
  const message = useRef({ name: "", body: "" }).current;
  return (
    <div>
      <div>
        <button
          onClick={() => {
            messageModule.write(message);
          }}
        >
          送信
        </button>
      </div>
      <div>
        <label>
          名前
          <br />
          <input onChange={e => (message.name = e.target.value)} />
        </label>
      </div>
      <div>
        <label>
          メッセージ
          <br />
          <input onChange={e => (message.body = e.target.value)} />
        </label>
      </div>
    </div>
  );
} 
```

・メッセージ表示

```tsx:/front/src/MessageList.tsx
import React, { useEffect } from "react";
import { useModule } from "@jswf/redux-module";
import { MessageModule } from "./MessageModule";

export function MessageList() {
  const messageModule = useModule(MessageModule);
  //初回のみメッセージを読み込む
  useEffect(() => {
    messageModule.load();
  }, []);
  //メッセージをStoreから取得
  const messages = messageModule.getState("messages")!;
  return (
    <div>
      {messages.map(msg => (
        <div key={msg.id}>
          <hr />
          <div>
            [{msg.name}]{msg.created_at} -- ({msg.id})
          </div>
          <div>{msg.body}</div>
        </div>
      ))}
    </div>
  );
} 
```

・トップモジュール

```tsx:/front/src/index.tsx
import React from "react";
import * as ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import { ModuleReducer } from "@jswf/redux-module";
import { MessageForm } from "./MessageForm";
import { MessageList } from "./MessageList";

function App() {
  return (
    <>
      <MessageForm />
      <MessageList />
    </>
  );
}

const store = createStore(ModuleReducer);
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root") as HTMLElement
); 
```


#### 2.3.3 確認

データはemulaterを再起動すると消えます

#### emulaterの起動

```
npm start
```

#### 2.3.4 確認

```
http://localhost:5000/
```

以下のように表示されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267629/4c4aa555-3264-245f-513a-a19cb76756f3.png)



## ３．デプロイ

以下のコマンドを使ってしばらく待ちます

```
npm deploy
```

Hosting URLとして表示されたアドレスで、掲示板が表示されます
初回は書き込みに15秒くらいかかりますが、その後はすぐに応答するようになります

## ４．まとめ

　Firebaseへのアクセスはfunctionsを経由すると、フロントエンド側でキーの管理をする必要がなくなるので、とてもやりとりが楽になります。さらに全てをいったんhosting経由にすることによって、ドメインが分散しなくなり、開発環境と本番環境でAjaxでのアクセス先を調整する必要も無くなりました。こんなに色々出来るのに、これらが全部無料で使えるとは素晴らしい時代になったと思いました。