Help us understand the problem. What is going on with this article?

React + Redux + Firebaseで作るTodo App

More than 3 years have passed since last update.

最初に

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

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

アプリ構成

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

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

キモは、

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

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

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

redux.png
参考:https://developers.eure.jp/tech/redux_feature/

Firebaseへのデプロイ

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

Firebaseでプロジェクトを作成する。

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

スクリーンショット 2016-10-08 12.52.09.png

そうすると、firebaseのdashboardが表示されるはずです。
3. Hostingタブを選択「スタートタブ」をクリック。
4. ステップがあるのでそれに従う。(次以降は下記で)firebaseをコマンドラインから扱うツールをインストールします。

$npm install -g firebase-tools <- firebaseをコマンドラインで扱うtool

プロジェクト作成

デプロイするためにプロジェクトを作成します。
今回は、React + Reduxが対象ではないので、
reduxのexampleをes6で書き直したプロジェクトを使いたいと思います。
githubにあげています。

因みに、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

以下のフォルダ構成が出来上がります。
スクリーンショット 2016-10-17 10.39.40.png

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

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

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

$npm run build
$firebase deploy

デプロイを確認

Hostingでデプロイを確認します。(因みにデプロイ履歴からロールバックすることができます)
スクリーンショット 2016-10-16 15.27.43.png

ドメインにアクセスすると無事ホスティングできてることがわかると思います。
スクリーンショット 2016-10-17 10.46.18.png

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

データベースと連携

Firebase導入

npm i -S firebase

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

/firebase/config.js
export const firebaseConfig = {
  apiKey: "自身のconfig",
  authDomain: "自身のconfig",
  databaseURL: "自身のconfig",
  storageBucket: "自身のconfig",
  messagingSenderId: "自身のconfig"
};

configはFirebase -> Overview -> ウェブアプリにFirebaseを追加する
スクリーンショット 2016-10-09 0.10.08.png

/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になるため、ルールを変更します。

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

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

Actionの変更

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

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

/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'
の値を購読すれば、各処理ごとに購読することができます。

/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は不要です。

/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を変更します。

/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を変更します。

/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に直します。

/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にデプロイします。

$ npm run build
$ firebase deploy

ホスティングからサイトを開きます。
htmlview-memory-increase.gif

無事データベースに書き込みができて、永続化されたことが確認できました。
スクリーンショット 2016-10-17 19.33.01.png

最後に

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

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

githubに完成版をあげています。ただし、/firebase/config.jsの値は削除しています。

参考

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

gonta616
Web & アプリエンジニアです。関西在住。フロントから、サーバーサイドまで幅広くやっています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away