JS初心者がReact+Firebaseで簡単なToDoListを作る

## 概要
最近仕事でReactを触るようになったので、それに関連して何か書けないかなと。
何が良いかな・・そういえばFirebaseってよく耳にするけど触ったことないな・・(汗)

というわけで、ReactとFirebaseで簡単なアプリを作ってみます。

## 作ったもの
- ToDoを一覧に登録/表示する簡単なアプリを作りました
- https://todolist-b715e.firebaseapp.com

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/0411ebbe-6c10-8b33-2314-c12ea1992718.gif)

- createボタンを押すと一覧に追加する
- doneのボタンを押すと該当のToDoを閉じて、タイトルに取り消し線を引く

## Reactとは
[Reactの公式ライブラリ](https://ja.reactjs.org/)
> React はユーザーインターフェースを作成する為の JavaScript のライブラリです。

つまり、UIのパーツ（構成部品）を作るためのライブラリです。
Facebook社がOSSとして公開しています。
開発を行っているFacebookやInstagramはもちろん、YahooやAirbnbなどの著名な企業が利用しています。

特徴ですが、以下の三点が公式にコンセプトとして掲げられています。

1. Declarative(宣言的である)
2. Component-Based(コンポーネント指向である)
3. Learn Once, Write Anywhere(一度の学習で何度でも書ける)

詳しくは[こちら](https://geekly.co.jp/column/cat-technology/1903_078/)をご参照ください。

## Firebaseとは
https://firebase.google.com/
Googleが提供しているBaaS(mobile Backend as a Service)
アプリケーションのバックエンドとして利用することのできるサービスで、主にモバイルアプリの開発に用いられています。
これを利用することでサーバレスな開発を実現でき、バックエンド側の開発コストを抑えることができます。

機能はたくさんありますが、全部を一度に理解するのは難しそうなので
今回は以下の二つの機能を使ってみようと思います。

- Webホスティング機能

 - Webサイトを手軽に公開できる

- Cloud Firestore(リアルタイムデータベース)
 - ドキュメント指向のNoSQLデータベース
 - リアルタイム性を兼ね備えており、ドキュメントに変更があった場合にも変更を即座に受け取ることも可能(クライアント側でリッスンしていた場合)

## 用いる技術
- フロントエンド: React.js
- バックエンド: Firestore

## サーバーサイド(Firestore)のプロジェクト作成
- [Firebase](https://firebase.google.com/)にアクセスし、Googleアカウントでログインする
- プロジェクトを作成する
![スクリーンショット 2019-12-16 3.37.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/3967fc20-c69d-cd43-b378-ae288bc594c5.png)
![スクリーンショット 2019-12-16 3.38.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/33961548-49d3-369c-485c-800d960f522c.png)
![スクリーンショット 2019-12-16 3.38.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/d4748bec-6cf9-3557-05d8-3c674ffbe5c5.png)
![スクリーンショット 2019-12-16 3.39.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/67ae5cb9-3c56-a63c-261c-0767b67068b0.png)

- データベースをテストモードで作成する
![スクリーンショット 2019-12-16 3.42.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/78506f19-53be-eadc-110e-ab4541c497f2.png)
![スクリーンショット 2019-12-16 3.42.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/d9cf4ffb-c0cb-77a0-6fdd-295e64c8ed43.png)
![スクリーンショット 2019-12-16 3.43.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/13194164-159e-e7f2-6ca8-94d6851dea3b.png)
- 何も登録されていない(当たり前)
![スクリーンショット 2019-12-16 3.44.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/85cbe30e-6af3-3642-2edf-a80dc0e352c5.png)

## クライアントサイド(React)のプロジェクト作成
- 今回は`create-react-app`を用いてReactのプロジェクトを作成します。
- ※npm(npx含む)コマンド実行環境の構築手順は[こちら](https://qiita.com/PolarBear/items/62c0416492810b7ecf7c)を参照

```console
$ npx create-react-app todolist-client
```
- ディレクトリを移動する

```console
$ cd todolist-client
```
- Reactのパッケージをインストールする

```console
$ npm i -S react@16.7.0-alpha.2 react-dom@16.7.0-alpha.2
```
- 開発用のサーバーを起動する

```console
$ npm start
```
- `http://localhost:3000/`にアクセスして以下の画面が表示されればプロジェクト作成完了です
![スクリーンショット 2019-12-16 4.17.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/743feaa2-6e75-4356-5933-275e58099363.png)
(何度やっても楽チンで感動する)

## 画面の見た目を作る

クラスを作成/編集する前にUIを作るためのライブラリを2つ

- [React Bootstrap](https://react-bootstrap.github.io/)
 - Bootstrap のコンポーネントをJSXで扱えるようにしたライブラリ

- [styled-components](https://www.styled-components.com/) 
 - css属性の情報を定義したタグ付きテンプレートリテラルを扱えるようにしたライブラリ

- 各ライブラリをインストールする

```console
$ npm install react-bootstrap bootstrap
``` 
```console
$ npm install styled-components@beta
```

- `src/App.js`に以下を追記する

```javascript:src/App.js
import 'bootstrap/dist/css/bootstrap.min.css';
```

- 以下のクラスを作成する

```jsx:src/components/CardList.js
import React from "react";
import styled from "styled-components";
import { Button, Card } from "react-bootstrap";

const StyledCard = styled(Card)`
  margin-top: 10px;
`;

const StyledCardBody = styled(Card.Body)`
  display: flex;
  flex-direction: column;
`;

const StyledDiv = styled.div`
  margin: auto 0px auto auto;
`;

const StyledButton = styled(Button)`
  align-self: flex-end;
`;

const CardList = ({ thread }) => {
  return (
    <>
      <StyledCard>
        <Card.Header
          style={{
            textDecoration: thread.doneFlg ? "line-through" : ""
          }}
        >
          {thread.title}
        </Card.Header>
        {!thread.doneFlg && (
          <StyledCardBody>
            <Card.Text>{thread.content}</Card.Text>
            <StyledDiv>
              <StyledButton variant="success">done</StyledButton>
            </StyledDiv>
          </StyledCardBody>
        )}
      </StyledCard>
    </>
  );
};

export default CardList;
```

```jsx:src/components/InputForm.js
import React, { useState } from "react";
import { Button, Form } from "react-bootstrap";
import styled from "styled-components";

const StyledForm = styled(Form)`
  display: flex;
  flex-direction: column;
`;

const StyledDiv = styled.div`
  align-self: flex-end;
`;

const InputForm = () => {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");

  return (
    <StyledForm>
      <StyledForm.Group controlId="title">
        <StyledForm.Control
          as="textarea"
          value={title}
          placeholder="タイトルを入力してください"
          onChange={event => setTitle(event.target.value)}
        />
      </StyledForm.Group>
      {title && (
        <StyledForm.Group controlId="content">
          <StyledForm.Control
            as="textarea"
            value={content}
            rows="3"
            placeholder="概要を入力してください"
            onChange={event => setContent(event.target.value)}
          />
        </StyledForm.Group>
      )}
      <StyledDiv>
        <Button id="create" disabled={!title || !content} variant="primary">
          create
        </Button>
      </StyledDiv>
    </StyledForm>
  );
};

export default InputForm;
```

```jsx:src/components/PageHome.js
import React, { useState } from "react";
import styled from "styled-components";
import CardList from "./CardList";
import InputForm from "./InputForm";

const StyledDiv = styled.div`
  width: 80%;
  margin: auto;
`;

const PageHome = () => {
  const [threads, setThreads] = useState([]);

  return (
    <StyledDiv>
      <h2>Input</h2>
      <InputForm />
      <h2>List</h2>
      {threads.map(thread => (
        <CardList key={thread.id} thread={thread} />
      ))}
    </StyledDiv>
  );
};

export default PageHome;
```
- `src/App.js`を修正する

```jsx:src/App.js
import "bootstrap/dist/css/bootstrap.min.css";
import React from "react";
import PageHome from "./components/PageHome";

const App = () => {
  return <PageHome />;
};

export default App;
```
- 開発用サーバーを起動して確認する

```console
$ npm start
```
- 以下の画面が表示されれば見た目の作成は完了です

![スクリーンショット 2019-12-16 5.19.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/9f821997-0a16-1977-191b-e1cc2a2797f4.png)
(まだ何もない)

## Firestoreとの通信処理を実装する
- Firebaseをインストールする

```console
$ npm i -S firebase
```

- Firebaseのトップページからアプリを登録する
![スクリーンショット 2019-12-16 5.27.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/29bdb7fb-3f8a-f573-6225-c2f8297de59e.png)
![スクリーンショット 2019-12-16 5.27.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/ec8c4ddb-531b-40a1-ee5b-9b4882f14acc.png)

- 以下のようなスクリプトが表示されるので、`var firebaseConfig`の値をコピーして`src/helpers/initializeApp.js`を作成する
![スクリーンショット 2019-12-16 5.30.42.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/c709619d-6692-6ce2-8199-e0e821c02d21.jpeg)
(キー情報なので伏せました)

```javascript:src/helpers/initializeApp.js
import { firestore, initializeApp } from 'firebase/app';
import 'firebase/firestore';

initializeApp({
    apiKey: '',
    authDomain: '',
    databaseURL: '',
    projectId: '',
    storageBucket: '',
    messagingSenderId: '',
    appId: '',
    measurementId: '',
});

firestore().settings({ timestampsInSnapshots: true });

firestore()
    .enablePersistence({ experimentalTabSynchronization: true })
    .catch(err => {
        console.error(err);
    });
```
- `src/index.js`を修正する

```javascript:src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
// 以下行を追加する
import './helpers/initializeApp.js';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

if (module.hot) {
    module.hot.accept();
}
```
- RxFireをインストールする

```console
$ npm i -S rxfire
```
- `src/helpers/create.js`を作成する

```javascript:src/helpers/create.js
import { firestore } from "firebase/app";

const create = async input => {
  const now = firestore.Timestamp.now();

  const systemFields = { createdAt: now };

  const threadRef = firestore()
    .collection("threads")
    .doc();

  await threadRef.set({
    ...systemFields,
    title: input.title,
    content: input.content,
    doneFlg: 0
  });
};

export { create };
```
- 以下クラスを修正する

```jsx:src/components/CardList.js
import React from "react";
import styled from "styled-components";
import { Button, Card } from "react-bootstrap";
import { firestore } from "firebase/app";

const StyledCard = styled(Card)`
  margin-top: 10px;
`;

const StyledDiv = styled.div`
  margin: auto 0px auto auto;
`;

const StyledButton = styled(Button)`
  align-self: flex-end;
`;

const StyledCardHeader = styled(Card.Header)``;

const StyledCardBody = styled(Card.Body)`
  display: flex;
  flex-direction: column;
`;

const CardList = ({ thread }) => {
  const onSubmit = () => {
    firestore()
      .collection("threads")
      .doc(thread.id)
      .update({
        title: thread.title,
        content: thread.content,
        doneFlg: 1,
        createdAt: thread.createdAt
      });
  };

  return (
    <>
      <StyledCard>
        <StyledCardHeader
          style={{
            textDecoration: thread.doneFlg ? "line-through" : ""
          }}
        >
          {thread.title}
        </StyledCardHeader>
        {!thread.doneFlg && (
          <StyledCardBody>
            <Card.Text>{thread.content}</Card.Text>
            <StyledDiv>
              <StyledButton variant="success" onClick={onSubmit}>
                done
              </StyledButton>
            </StyledDiv>
          </StyledCardBody>
        )}
      </StyledCard>
    </>
  );
};

export default CardList;
```
```jsx:src/components/InputForm.js
import React, { useState } from "react";
import { Button, Form } from "react-bootstrap";
import styled from "styled-components";
import { create } from "../helpers/create";

const StyledForm = styled(Form)`
  display: flex;
  flex-direction: column;
`;

const StyledDiv = styled.div`
  align-self: flex-end;
`;

const InputForm = () => {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");

  const onSubmit = () => {
    if (title || content)
      create({ title, content })
        .then(() => {
          setTitle("");
          setContent("");
        })
        .catch(err => {
          console.error(err);
        });
  };

  return (
    <StyledForm>
      <StyledForm.Group controlId="title">
        <StyledForm.Control
          as="textarea"
          value={title}
          placeholder="タイトルを入力してください"
          onChange={event => setTitle(event.target.value)}
        />
      </StyledForm.Group>
      {title && (
        <StyledForm.Group controlId="content">
          <StyledForm.Control
            as="textarea"
            value={content}
            rows="3"
            placeholder="概要を入力してください"
            onChange={event => setContent(event.target.value)}
          />
        </StyledForm.Group>
      )}
      <StyledDiv>
        <Button
          id="create"
          disabled={!title || !content}
          onClick={onSubmit}
          variant="primary"
        >
          create
        </Button>
      </StyledDiv>
    </StyledForm>
  );
};

export default InputForm;
```
```jsx:src/components/
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import { firestore } from "firebase/app";
import { collectionData } from "rxfire/firestore";
import CardList from "./CardList";
import InputForm from "./InputForm";

const StyledDiv = styled.div`
  width: 80%;
  margin: auto;
`;

const PageHome = () => {
  const [threads, setThreads] = useState([]);

  const query = firestore()
    .collection("threads")
    .orderBy("createdAt", "desc");

  useEffect(() => {
    const subscription = collectionData(query, "id").subscribe(data => {
      setThreads(data);
    });
    return () => subscription.unsubscribe();
  }, []);

  return (
    <StyledDiv>
      <h2>Input</h2>
      <InputForm />
      <h2>List</h2>
      {threads.map(thread => (
        <CardList key={thread.id} thread={thread} />
      ))}
    </StyledDiv>
  );
};

export default PageHome;
```
- 開発用サーバーを起動して確認する

```console
$ npm start
```
- 以下のような動きをすれば通信処理の実装は完了です
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/e33b620c-23fc-3bc3-1535-1cb8c4b8134b.gif)

- データベースには以下のような形式で値が登録されます
![スクリーンショット 2019-12-16 7.01.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/2eed02ad-5fc1-bc22-18d7-2142219549b6.png)
(ついに登録された！)

## ホスティングする
- 本番用にビルドする

```console
$ npm run build
```
- `.firebaserc`を作成する

```javascript:.firebaserc
{
  "projects": {
    "default": "プロジェクトID"
  }
}
```
- プロジェクトIDはFirebaseの管理画面から確認できる
![スクリーンショット 2019-12-16 7.15.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/76159641-6de8-89fb-1838-7e9ca1c3b5a6.png)

- `firebase.json`を作成する

```json:firebase.json
{
  "hosting": {
    "public": "build",
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}
```
- Firebase-toolsをインストールする

```console
$ npm i -g firebase-tools
```
- Firebaseにログインする

```console
$ firebase login
```
- Webページを公開する

```console
$ firebase deploy --only hosting
```
- コンソールに表示されるURLにアクセスする
![スクリーンショット 2019-12-16 7.20.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/738e3dd5-70b4-f486-f292-f7826054951f.png)

- ホスティングしたWebページが表示される
![スクリーンショット 2019-12-16 7.21.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504173/4deb8042-4bb0-470e-f0cc-d25edbb59570.png)

## 最後に
- Firebaseまだ全然使いこなせてない・・
- 次はモバイルアプリ開発にも挑戦したい


ここまで読んでいただいてありがとうございました。