React + Redux + Firebaseで作るTodo App

# 最初に
先日、[フロントエンジニアが今話題のFirebaseについて語りたい](http://qiita.com/gonta616/items/8896bcffa04fb00b9567)という記事を投稿しました。
予想以上の方にストック頂き、とても嬉しい反面驚きがすごいです。
改めてFirebaseは注目されてるサービスで、サーバーレスアーキテクチャはフロントエンドエンジニア & アプリ開発者の夢なんだなぁと実感しました。

ポエミーで長い記事になってましたが、
設計&具体的な実装については一切触れていなかったので、その辺りを書きたいと思います。
まだまだ勉強中の身のため、ご指摘いただけると幸いです。

# アプリ構成

* React.js (UI)
* Redux (データフロー)
* Firease (ホスティング + データベース)

でTodo Appを作りたいと思います。

キモは、

* Firebaseのデプロイ
* データベースと連携

です。
React + Reduxで作るのはスキルセットがそうなのに加えて、**Reduxと相性が良い** ところです。
Reduxのデータフローが一方向であること、HTTP通信のActionがピュアjavascriptのため、影響を受けにくいのが印象です。

参考図の通り、ReduxはActionでリクエストを発行するのが推奨されています。
赤枠の部分を変更するだけで、Firebaseの導入ができることを目指します。

<img width="604" alt="redux.png" src="https://qiita-image-store.s3.amazonaws.com/0/56423/fe9ccea4-70b7-00c5-2532-f0402290da05.png">
参考:https://developers.eure.jp/tech/redux_feature/

# Firebaseへのデプロイ
まずは、Firebaseへのホスティングを行います。デプロイのシンプルさを紹介できればと思います。

## Firebaseでプロジェクトを作成する。
1. https://console.firebase.google.com/ にアクセスします。(google アカウントが必要です)
2. 「新規プロジェクトを作成」からプロジェクトを作成します。

<img width="451" alt="スクリーンショット 2016-10-08 12.52.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/56423/84ca5359-e9e3-3302-f308-d6e58b85d24b.png">

そうすると、firebaseのdashboardが表示されるはずです。
3. Hostingタブを選択「スタートタブ」をクリック。
4. ステップがあるのでそれに従う。(次以降は下記で)firebaseをコマンドラインから扱うツールをインストールします。
```
$npm install -g firebase-tools <- firebaseをコマンドラインで扱うtool
```
## プロジェクト作成
デプロイするためにプロジェクトを作成します。
今回は、React + Reduxが対象ではないので、
[reduxのexample](https://github.com/reactjs/redux/tree/master/examples/todos)をes6で書き直したプロジェクトを使いたいと思います。
[github](https://github.com/gonta616/react-redux-todos)にあげています。

因みに、`create-react-app`を使いました。
下記に詳細がありますが、Reactの一番の障壁はReactの学習より、環境構築だと思ってますので、本当に感謝しかありません。React導入がめちゃめちゃ楽になりました。
http://qiita.com/chibicode/items/8533dd72f1ebaeb4b614

## Firebaseと紐付ける。

Firebaseのログインと、初期化処理を行います。

```
$firebase login <- 同じgoogle アカウントとパスワードでログイン
$firebase init <- Firebaseとの紐づけ
> Hosting: Configure and deploy Firebase Hosting sites
> ❯ firebase-react-redux-todos (fir-react-redux-todos) // 自分の作成したプロジェクト
? What file should be used for Database Rules? (database.rules.json) // Enter
```

以下のフォルダ構成が出来上がります。
<img width="219" alt="スクリーンショット 2016-10-17 10.39.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/56423/4e24aa0b-5c20-db37-5049-54e375ef118f.png">

`npm run build`はbuildフォルダを作成します。firebaseのデプロイ先のdefaultはpublicなので`firebase.json`の設定を変更します。

```json:firebase.json
{
  "database": {
    "rules": "database.rules.json"
  },
  "hosting": {
    "public": "build"
  }
}
```

buildフォルダを作成後、firebaseにデプロイします。

```bash
$npm run build
$firebase deploy
```

## デプロイを確認
Hostingでデプロイを確認します。(因みにデプロイ履歴からロールバックすることができます)
<img width="1023" alt="スクリーンショット 2016-10-16 15.27.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/56423/591c2b16-88a1-f6bf-e902-64cc55576810.png">

ドメインにアクセスすると無事ホスティングできてることがわかると思います。
<img width="455" alt="スクリーンショット 2016-10-17 10.46.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/56423/6311c466-2075-cf85-655c-33a6ee673670.png">

ただし、reduxはローカルストレージに保存されるため、リフレッシュするとデータが失われます。
データを永続化するためにはデータベースに保存する必要があります。
これを解決するためにFirebaseのリアルタイムデータベースを使用したいと思います。

# データベースと連携

## Firebase導入
`npm i -S firebase`

firebaseを作ります。
 -config.js  // Firebaseの設定
 -index.js   // DBのインスタンス化

```javascript:/firebase/config.js
export const firebaseConfig = {
  apiKey: "自身のconfig",
  authDomain: "自身のconfig",
  databaseURL: "自身のconfig",
  storageBucket: "自身のconfig",
  messagingSenderId: "自身のconfig"
};
```
configはFirebase -> Overview -> ウェブアプリにFirebaseを追加する
<img width="775" alt="スクリーンショット 2016-10-09 0.10.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/56423/ca23b774-5dcf-0f20-12d8-26451ec0451e.png">

```javascript:/firebase/index.js
import firebase from 'firebase';
import { firebaseConfig } from './config';

export const firebaseApp = firebase.initializeApp(firebaseConfig);
export const firebaseDb = firebaseApp.database();
```
configファイルからFirebaseをインスタンス化して、データベースの参照を作成する。

## データベースのルールを設定する
現時点では、セキュリティルールに引っかかりpermission errorになるため、ルールを変更します。

```json:database.rules.json
{
  "rules": {
    ".read":"true",
    ".write":"true"
  }
}
```

データベースのルールを反映させるため一度デプロイします。`firebase deploy`

## Actionの変更
Reduxでは、ActionからHTTP通信するのが推奨されています。
ActionからFirebaseのデータベースにアクセスします。

'todos'のデータにアクセスしたいため、パスを設定します。

```javascript:/actions/todo.js
import {firebaseDb} from '../firebase/'
const ref = firebaseDb.ref('todos');
```

todo.jsにfirebaseの購読処理を追記します。
'value'を購読していると、firebaseの'todos'が変更されるたび、処理が実行されます。具体的に言うと、'todos'の最新の状態を取得します。

※今回は、最新状態を取得していますが
.once('value' ...
で一度だけ購読し、'child_added','child_changed','child_removed'
の値を購読すれば、各処理ごとに購読することができます。

```javascript:/actions/todo.js
// Subscribe
function loadTodos() {
  return dispatch => {
    ref.off()
    // valueを購読する。todosに変更があれば、以下の処理が実行される。
    ref.on('value',
      (snapshot) => {dispatch(loadTodosSuccess(snapshot))},
      (error) => {dispatch(loadTodosError(error))}
    )
  }
}

function loadTodosSuccess(snapshot){
  return {
    type: 'TODOS_RECEIVE_DATA',
    data: snapshot.val()
  }
}

function loadTodosError(error){
  return {
    type: 'TODOS_RECIVE_ERROR',
    message: error.message
  }
}
```

addTodoを変更します。
pushされると、'value'のイベントが実行されるため、action typeは不要です。

```javascript:/actions/todo.js
(before)
let id = 0;
function addTodo(text){
  return {
    type: 'ADD_TODO',
    data: {
      key: id++,
      text: text,
      completed: false,
    }
  }
}

（after）
function addTodo(text){
  return dispatch => {
    ref.push({
      text: text,
      completed: false,
    })
    .catch(error => dispatch({
      type: 'ADD_TASK_ERROR',
      message: error.message,
    }));
  }
}
```

updateTodoを変更します。

```javascript:/actions/todo.js
(before)
function updateTodo(key){
  return {
    type: 'CHANGE_TODO',
    data: {
      key: key,
    }
  }
}

(after)
function updateTodo(key){
  return (dispatch, getState) => {
    let state = getState()
    let todo = state.todos.filter(todo => todo.key === key)
    
    // パスのオブジェクトをアップデートします。
    // updateにはオブジェクトを渡すと差分を自動で更新してくれます。 
    firebaseDb.ref(`todos/${key}`).update({completed: !todo[0].completed})
    .catch(error => dispatch({
      type: 'UPDATE_TASK_ERROR',
      message: error.message,
    }));
  }
}
```

deleteTodoを変更します。

```javascript:/actions/todo.js
(before)
function deleteTodo(key){
  return {
    type: 'DELETE_TODO',
    data: {
      key: key,
    }
  }
}

(after)
function deleteTodo(key){
  return dispatch => {
   // パスのオブジェクトを削除します。
   firebaseDb.ref(`todos/${key}`).remove()
    .catch(error => dispatch({
      type: 'DELETE_TASK_ERROR',
      message: error.message,
    }));
  }
}
```

## Reducerの変更
actionTypeを変更したので、Reducerも変更します。
'ADD_TODO', 'CHANGE_TODO','DELETE_TODO'が'TODOS_RECEIVE_DATA'に一本化されました。Firebaseからはオブジェクトが返ってくるので、Arrayに直します。

```javascript:/actions/todo.js
(before)
  switch (action.type) {
    case 'ADD_TODO':
    return [
      ...state,
      {
        key: action.data.key,
        text: action.data.text,
        completed: action.data.completed,
      }
    ]

    case 'CHANGE_TODO':
      state.map(todo => {
        if(todo.key === action.data.key){
          todo.completed = !todo.completed;
        }
      });
      return [
        ...state
      ]

    case 'DELETE_TODO':
      let n = state.filter(todo => todo.key !== action.data.key)
      return [
        ...n
      ]
    default:
      return state
  }

(after)
  switch (action.type) {
    case 'TODOS_RECEIVE_DATA':
      let todos = []
      if (action.data){
        Object.keys(action.data).forEach(key =>{
          let todo = action.data[key];
          todos.push({
            key: key,
            text: todo.text,
            completed: todo.completed,
          })
        });
      }
      return [...todos]

    case 'TODOS_RECIVE_ERROR':
    case 'ADD_TASK_ERROR':
    case 'UPDATE_TASK_ERROR':
    case 'DELETE_TASK_ERROR':
      alert(action.message)

    default:
      return state
  }

```

## 再びデプロイ

* ActionをFirebaseと連携させる。
* Reducerを変更する。

大分、簡単にしましたが、上記の2つの変更で
React + Redux -> React + Redux + Firebaseの切り替えが完了しました。
Reduxさえキチンと構築していれば、React周りを弄ることなく、Firebaseが導入できることがわかると思います。

再コンパイルして、Firebaseにデプロイします。

```bash
$ npm run build
$ firebase deploy
```

ホスティングから[サイト](https://fir-react-redux-todos.firebaseapp.com/)を開きます。
![htmlview-memory-increase.gif](https://qiita-image-store.s3.amazonaws.com/0/56423/d9f26089-0a83-98dd-6754-0fe3d8633564.gif)

無事データベースに書き込みができて、永続化されたことが確認できました。
<img width="896" alt="スクリーンショット 2016-10-17 19.33.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/56423/7af5bef4-0d1a-2d06-eec6-ab6ff9162859.png">

# 最後に
今回作ったのは簡単なTodo Appでしたが、まるでReduxの延長線のようにFirebaseが導入できたと思います。
アプリがスケールしても構造が変わりにくいですし、Actionで完結するので他のRedux moduleの影響も受けにくいです。

これまで必要だった、サーバー立てて、JSON APIを返すようのサーバー処理をがっつり削除することができました。
Firebase素敵。

[githubに完成版をあげています。](https://github.com/gonta616/firebase-react-redux-todos)ただし、/firebase/config.jsの値は削除しています。

# 参考
https://github.com/vkammerer/react-redux-firebase
※ action周りを参考にしています。
https://github.com/r-park/todo-react-redux 
※今回は一つずつ進めることを重視しているので参考程度ですが、Firebaseのcodeを切り離した、実践的で綺麗なコードです。