TypeScript + React + styled-components + Firestore + Firebase HostingでWebアプリを作り始める


[TypeScript](https://www.typescriptlang.org/), [React](https://reactjs.org/), [styled-components](https://www.styled-components.com/), [Cloud Firestore](https://firebase.google.com/docs/firestore/?hl=ja), [Firebase Hosting](https://firebase.google.com/docs/hosting/?hl=ja)を使ってWebサイトを公開する方法。[^1]

「<font color="orange">FirestoreもReactも使ったことない人(筆者もそう)</font>」が
「**Firestoreに接続するReactアプリ**を作り、**Firebase Hosting**にデプロイするまで」を簡潔にまとめました。[^2]

* VSCodeを使っています
* 色々な考慮を省略しています

## 1. Reactプロジェクトの作成

### まずは必要なものインストール

#### [firebase-tools](https://github.com/firebase/firebase-tools)
公式のコマンドラインツール

```shell
$ npm install -g firebase-tools
```

#### [create-react-app](https://github.com/facebook/create-react-app)
Facebook公式。Reactアプリの雛形を生成できる。

```shell
$ npm install -g create-react-app
```

### 雛形の生成
任意の場所で以下を実行するだけ。
オプション「[--scripts-version=react-scripts-ts](https://github.com/wmonk/create-react-app-typescript)」により、TypeScriptを使う構成で生成してくれる。

```shell
$ create-react-app hoge-react-app --scripts-version=react-scripts-ts
```

### VSCodeの拡張機能や設定

- [Firebase](https://marketplace.visualstudio.com/items?itemName=toba.vsfire)
- [TSlint](https://marketplace.visualstudio.com/items?itemName=eg2.tslint) : [tslint](https://palantir.github.io/tslint/rules/)と[tslint-react](https://github.com/palantir/tslint-react)を参考に設定する
- [TypeScript Hero](https://marketplace.visualstudio.com/items?itemName=rbbit.typescript-hero): importの自動修正
- [styled-components](https://github.com/styled-components/vscode-styled-components)
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)

【Prettierについての補足】
上記のプラグインを入れた後、設定に以下を追加するだけで済む。

```json
"[typescriptreact]": {
    "editor.formatOnSave": true
},
```
[フォーマットのルール](https://prettier.io/docs/en/options.html)をカスタマイズしたい場合は、`.prettierrc`を作成して定義すればOK。

```..prettierrc.json
{
  "singleQuote": true
}
```

## 2. Firebase Hostingへのデプロイ

### Firebaseプロジェクトの生成
「[無料で開始](https://firebase.google.com/products/hosting/?hl=ja)」ボタンからプロジェクトを作成する。
Databaseの項目からFirestoreも開始しておく。

### ターミナルからFirebaseにログインする
```shell
$ firebase login
```

### Reactアプリをfirebase Hostingと接続する　
`hoge-react-app`ディレクトリで以下を実行する

```shell
$ firebase init
// 「◯ Hosting: Configure and deploy Firebase Hosting sites」をスペースキーで選んでEnter
// さっき作成したプロジェクトを選んでEnter
```

### firebase.jsonの編集
hostingのpublicを"build"にしておく。

```firebase.json
{
  "hosting": {
    "public": "build",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}
```

### 動作確認
```shell
$ npm start
```

### deployしてみる
```shell
$ npm run build && firebase deploy
```
ここまでは特に面倒なこともなくスムーズにいける(はず)

## 3. Firestoreとの接続
こっからが本記事の本番
最終的に↓のようになる。
<img width="192" alt="スクリーンショット 2018-10-22 13.39.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/159680/694d14e1-5d6b-1db3-2143-9c2d3216ff71.png">

### [Firestoreのルール](https://firebase.google.com/docs/firestore/security/secure-data?hl=ja)を設定する
`hoge-react-app`ディレクトリのルートに、`firestore.rules`ファイルを新規作成する。
とりあえず、書き込みだけは認証が必要な設定にしてみる。

```firestore.rules
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read;
      allow write: if request.auth != null;
    }
  }
}
```

### Firebase関連のコードを追加
srcディレクトリ配下に、`firebase`ディレクトリを作成し、以下のファイルを新規作成します。
(ディレクトリ名はわかりやすく`firebase`にしただけで、別になんでもいい)
ちょっとこの実装最高に雑ですが...おゆるしを...

``` firebase/index.tsx
import * as firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import { FIREBASE_CONFIG } from "./config";

export default class Env {
  private static singleInstance: Env;

  public readonly firebase: firebase.app.App;
  public readonly firestore: firebase.firestore.Firestore;
  public readonly providerGoogle = new firebase.auth.GoogleAuthProvider();

  private constructor() {
    this.firebase = firebase.initializeApp(FIREBASE_CONFIG);
    this.firestore = this.firebase.firestore();
    const settings = { timestampsInSnapshots: true };
    this.firestore.settings(settings);
  }

  public static get instance(): Env {
    if (!this.singleInstance) {
      this.singleInstance = new Env();
    }
    return this.singleInstance;
  }
}
```

```firebase/config.tsx
// 以下の情報は、Firebase-consoleから、「+アプリを追加」 > 「 </> 」を選ぶと確認できます。
export const FIREBASE_CONFIG = {
  apiKey: 'XXX',
  authDomain: 'XXX',
  databaseURL: 'XXX',
  projectId: 'XXX',
  storageBucket: 'XXX',
  messagingSenderId: 'XXX'
};
```

### App.tsxの編集
srcディレクトリ配下のApp.tsxを編集し、Firestoreと接続しましょう。[^3]
ボタン2つを配置するだけの単純なものです。

```App.tsx
import * as React from "react";
import * as Boostrap from "react-bootstrap";
import "./App.css";
import Env from "./firebase/index";

export default class App extends React.Component {
  public render(): JSX.Element {
    return (
      <div className="App">
        <Boostrap.Button onClick={this.handleLogin}>Login</Boostrap.Button>
        <Boostrap.Button onClick={this.insertDummyUser}>
          firestore test
        </Boostrap.Button>
      </div>
    );
  }

  private handleLogin = () => {
    Env.instance.firebase
      .auth()
      .signInWithRedirect(Env.instance.providerGoogle);
    Env.instance.firebase
      .auth()
      .getRedirectResult()
      .then(result => {
        console.log(result);
      })
      .catch(error => {
        console.log(error);
      });
  };

  private insertDummyUser = () => {
    Env.instance.firestore
      .collection("users")
      .doc("dummy_user_1")
      .set({
        country: "Japan",
        name: "dummyUser1"
      })
      .then(() => {
        console.log("Success: Document has written");
      })
      .catch(error => {
        console.error("Error writing document: ", error);
      });
  };
}
```

### 完成
こんな感じになっているはず
<img width="192" alt="スクリーンショット 2018-10-22 13.39.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/159680/694d14e1-5d6b-1db3-2143-9c2d3216ff71.png">

### 動作確認
```shell
$ npm start
```
こんな感じの画面が出ます。
<img width="768" alt="スクリーンショット 2018-10-18 16.38.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/159680/7523e144-3da1-af6b-93ed-9850f57388a4.png">

Loginしていなければ、Firestoreへのコレクション追加が失敗するのを確認しましょう。(デベロッパーツールにエラーが出るはず)
また、Fireabse-consoleから、コレクションの追加を確認しましょう。

### deploy！
```shell
$ npm run build && firebase deploy
```

## 4. CSS in JSをやっていく
　最後に、[styled-components](https://www.styled-components.com/)を導入し、CSSを少し書いてみます。[^4]

### まず必要なものインストール
```shell
$ npm install --save styled-components
$ npm install @types/styled-components
```

### App.tsxにCSSを追加
[styled-components](https://www.styled-components.com/)のサンプルCSSを参考に、ボタンの色を変えてみます。

```App.tsx
import * as React from 'react';
import styled, { css } from 'styled-components';
import './App.css';
import Env from './firebase/index';

export default class App extends React.Component {
  public render() {
    return (
      <div className="App">
        <Button onClick={this.handleLogin}>Login</Button>
        <Button primary={true} onClick={this.insertDummyUser}>
          firestore test
        </Button>
      </div>
    );
  }

  private handleLogin = () => {
    Env.instance.firebase
      .auth()
      .signInWithRedirect(Env.instance.providerGoogle);
    Env.instance.firebase
      .auth()
      .getRedirectResult()
      .then(result => {
        console.log(result);
      })
      .catch(error => {
        console.log(error);
      });
  };

  private insertDummyUser = () => {
    Env.instance.firestore
      .collection('users')
      .doc('dummy_user_1')
      .set({
        country: 'Japan',
        name: 'dummyUser1'
      })
      .then(() => {
        console.log('Success: Document has written');
      })
      .catch(error => {
        console.error('Error writing document: ', error);
      });
  };
}

// styled-components
interface InterfaceMyProps {
  primary?: boolean;
  onClick: () => void;
}

const StyledButton = styled.button`
  background: transparent;
  border-radius: 3px;
  border: 2px solid palevioletred;
  color: palevioletred;
  margin: 0 1em;
  padding: 0.25em 1em;

  ${(props: InterfaceMyProps) =>
    props.primary &&
    css`
      background: palevioletred;
      color: white;
    `};
`;

const Button: React.SFC<InterfaceMyProps> = ({ ...props }) => (
  <StyledButton {...props} />
);
```

ちょっとここらへん強引な感がある...悩

### 完成
<img width="327" alt="スクリーンショット 2018-10-20 17.08.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/159680/74296100-39de-7dfb-e63b-a34061d3c990.png">

あとはdeployするなり。

[^1]: 最初はGAE/GoとFirestoreで書いていましたが、とある重要なことに[デプロイして初めて気づきました](https://qiita.com/IJN-Penguin/items/b6252cd461b3966d53ac#comment-c60b324c726adde320c1)。
[^2]: TypeScript + Reactについては、[公式ドキュメント](https://www.typescriptlang.org/docs/handbook/react-&-webpack.html)や[この記事](https://mae.chab.in/archives/59782#post59782-1)が分かりやすい。
[^3]: このコードはPrettierの自動フォーマットに従ったままです。
[^4]: [公式ドキュメント](https://www.styled-components.com/docs/api#typescript)や[styled-componentsを使ったCSS設計](https://qiita.com/taneba/items/4547830b461d11a69a20)、[React + Storybook + styled-componentsをTypeScriptで動かす](https://qiita.com/Yuta_Yamamoto/items/7ca3797a8a7865b2c379)などを参考にしました。