react-redux-starter-kitとfirebaseでserverless/SPAなtodo listを作る

こんな感じの、よくあるtodo listを作ります：

<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/10487/487b21bc-7f87-0b93-6d0a-0fd52c66f397.gif">

個人的にjavascript書くこと自体が10年ぶりですが、昔の印象と全然違っていて結構刺激的です。そんな中で、最近すこしreactとfirebaseを触る機会があって、せっかくなので自分で調べながらやってみた結果について書いてみたいと思います。

まず、reactでSPAを実現して、その上でfirebaseという分散型リアルタイムデータベースを利用して、serverlessなサービスを実現します。なお、私自身はjavascriptに関してほぼ初心者と変わらないレベルなので、ライブラリを沢山組み立てるのではなく、react-redux-starter-kit[^1]を利用して作ります。

前提知識としては、react tutorial[^2]とfirebaseのFor Web/Get Start Guide[^3]を一通り読まれたことを想定しています。react、redux、react-router、webpackなどについての説明は色んなリソースがあるので、ここでは実際にどういう風に作っていくかを中心に書いていきたいと思います

# react-redux-starter-kitでfirebaseを利用する
react-redux-starter-kitからfirebaseを利用する方法を確認します。

## react-redux-starter-kit
まずは、react-redux-starter-kit[^1]をcloneして、`npm install`、`npm start`すれば、`localhost:3000`にて下記のアヒルが現れます：

<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/10487/49a87199-0872-41fe-379b-b3947a335d1f.png">

## routeを追加する
独立した新規画面を作りたいので、react-router的には新しいrouteを追加することになります。`src/routes/Counter`を真似して、`src/routes/Todo`を用意します：

 * `src/routes/Counter/components/Counter.js` => `src/routes/Todo/components/Todo.js`
 * `src/routes/Counter/containers/CounterContainer.js` => `src/routes/Todo/containers/TodoContainer.js`
 * `src/routes/Counter/modules/counter.js` => `src/routes/Todo/modules/todo.js`
 * `src/routes/index.js`に`TodoRoute`を追加する

[詳しくはこちらのコミット](https://github.com/konyavic/react-redux-firebase-todo/commit/8f50a3b178f2e37742040c15e44a0d6d41c14f67#diff-e1f803c0d4525859fb1897bbdf3964d2)

ついでに、starter-kitのヘッダーにリンクを追加しておきます：

```diff:src/components/Header/Header.js
 export const Header = () => (
   <div>
     <h1>React Redux Starter Kit</h1>
     <IndexLink to='/' activeClassName='route--active'>
       Home
     </IndexLink>
     {' · '}
      <Link to='/counter' activeClassName='route--active'>
        Counter
      </Link>
 +    {' · '}
 +    <Link to='/todo' activeClassName='route--active'>
 +      Todo
 +    </Link>
    </div>
  )
```

そしたら、ヘッダーから飛べるようになります：

<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/10487/c44d464c-9a2c-aec2-3b66-9952f88d80fb.png">

## firebaseの導入

firebaseのチュートリアルではlinkとscriptタグで直接引用してチャットアプリを作っていますが、ここはreactとwebpackの仕組みに合わせるために少し違います。

まず、firebaseアカウントの取得やproject createまではチュートリアルと一緒です。そして、コード内でimportするにはfirebaseを`npm install --save firebase`で入れます。

次に、`firebase.initializeApp`を最初に呼び出す必要があるので、とりあえず今回は`src/main.js`に書いておきます：

（ちなみに、firebaseのapiKeyなどはクライアント側に丸見えなので、秘密情報ではありません。その代わりに、databaseのsecurity rulesは慎重に決める必要があります）

```diff:src/main.js
  import React from 'react'
  import ReactDOM from 'react-dom'
 +import firebase from 'firebase'
  import createStore from './store/createStore'
  import AppContainer from './containers/AppContainer'
  
 +// Initialize Firebase
 +var config = {
 +  apiKey: "****",
 +  authDomain: "****",
 +  databaseURL: "https://****.firebaseio.com",
 +  storageBucket: "****.appspot.com",
 +  messagingSenderId: "****"
 +};
 +firebase.initializeApp(config);
 +
```

ここで一度read/writeが動くのを確認したいので、`componentDidMount`のタイミングでwrite->readしてみます：

（reducer/containerの修正を一部省きます）

```diff:src/routes/Todo/components/Todo.js
 +export class Todo extends React.Component {
 +  componentDidMount() {
 +    this.props.didMount()
 +  }
 +
 +  render() {
 +    let props = this.props
 +    return <div style={{ margin: '0 auto' }} >
 +      <h2>Todo</h2>
 +      foo: {props.foo}
 +    </div>
 +  }
 +}
```

```diff:src/routes/Todo/modules/todo.js
 +export function didMount() {
 +  return (dispatch, getState) => {
 +    firebase.database().ref("foo").set("bar").then(() => {
 +      firebase.database().ref("foo").once('value').then(data => {
 +        dispatch({
 +          type: "FOO_SET",
 +          payload: data.val(),
 +        })
 +      })
 +    })
 +  }
 +}
```

firebaseのsecurity rulesも一旦認証なしに切り替えます：

（危険なので、あとで必ず戻しましょう）

<img width="589" alt="スクリーンショット 2016-12-03 10.31.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/10487/c9188c47-8a2b-12d6-7d9c-70d9759bf142.png">

これで、もの自体はもう動きますが、webpack的には共通ライブラリーを`vendor.js`にまとめてダウンロードするので、firebaseも一緒に入れましょう：

```diff:config/index.js
   compiler_vendors : [
      'react',
      'react-redux',
      'react-router',
 -    'redux'
 +    'redux',
 +    'firebase'
    ],
```

さて、`localhost:3000/todo`にアクセスしてみましょう。書き込んだ`bar`がちゃんと反映されました：

<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/10487/c5a93721-7de7-d937-1389-899c9286ddff.png">

[ここまでの作業はこのコミット](https://github.com/konyavic/react-redux-firebase-todo/commit/66db7d0d7254c1b490669c1487160a54577aa470)

## firebaseの認証を通す

認証なしのsecurity rulesだと悪用される可能性があるし、ユーザ毎に状態を永続化したいので、ここでちゃんとfirebaseの認証を通すように改修します：

 * 認証通っていないときは、認証のボタンを表示します
  * そのボタンの`onClick`で`firebase.auth().signInWithPopup`を呼び出します 
 * `componentDidMount`のタイミングで`firebase.auth().onAuthStateChanged`のコールバックを設定します
  * ちなみに、`onAuthStateChanged`のコールバックは認証が通る前に必ず一度呼ばれるので、呼ばれたから認証が通ったわけではありません（戻り値のuserはnull）

[詳しくはこのコミット](https://github.com/konyavic/react-redux-firebase-todo/commit/be9870950c16dfd760187c3aa4b9653b62149cc2)

これで、一回目のアクセスは下記のように「auth」と書いてるボタンが表示され、そこをクリックすると認証ポップアップが出てきます。認証が通ったら、`props.isAuth`が更新されるので、画面は前の`foo: bar`に切り替わります：

<img width="709" alt="スクリーンショット 2016-12-01 18.20.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/10487/db69b146-edd2-5361-a9b6-253a7a7bdca6.png">

## `TodoView`を作り込む

これでやっと、react+firebaseの土台が完成しました。さて、ここからtodo listの中身を作っていきたいので、`TodoView`というコンポネントに分割して作業します：

```diff:src/routes/Todo/components/TodoView.js
 +import React from 'react'
 +
 +const TodoView = (props) => <div>
 +  foo: {props.foo}
 +</div>
 +
 +export default TodoView
```

しかし、分割したとは言え、動作確認のときは、毎回親コンポネントの`Todo`を通して認証が発生します。小さいプロジェクトならいいけど、認証が複雑なものを作ろうとすると大変ですね。

# sandbox routeで`TodoView`を作る

できれば、firebaseと切り離した状態で`TodoView`を作り込んでいきたいです。色んな方法があるかもしれませんが、私がたどり着いたのは結局、新たに`Sandbox`というrouteを用意して、そこで`TodoView`だけを表示するreducer/containerを置く方法です。（多分、もっと賢いが沢山あると信じているけど、できればライブラリーやmiddlewareを多用したくないのでこうなりました…）

## sandbox routeを追加する

では、最初に`Todo`のrouteを追加するときと同様に、`Sandbox`のrouteを追加します。ここで`Todo`と2点の違いがあります：

 * コンポネントを設計するのが目的なので、あまり複雑にならないreducerをcontainerに統合してもよいでしょう
 * `TodoView`のルートをsandboxの`childRoutes`に追加します

[詳しくはこのコミット] (https://github.com/konyavic/react-redux-firebase-todo/commit/6cd843c6d0b7707132d4e215c43427cb4443c0ff)

`localhost:3000/sandbox/todoview`にアクセスして、表示を確認します：

<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/10487/c511f78f-eada-cb1a-fa5a-4ff455800e98.png">

## 静的な`TodoView`を作り込む

これで、view以外のもの（いわゆるmodule/controller）に影響されない足場ができたので、`TodoView`の設計をどんどん作り込んでいきます。まずはtodoのデータを静的に書いて、`sandbox/todoview`で表示を確認します：

```diff:src/routes/Sandbox/containers/TodoViewContainer.js
  const mapStateToProps = (state) => ({
 -  foo: 123
 +  data: [
 +    {
 +      title: "仕事する",
 +      isDone: false,
 +    },
 +    {
 +      title: "アドベントカレンダーを書く",
 +      isDone: false,
 +    },
 +    {
 +      title: "寝る",
 +      isDone: false,
 +    },
 +  ]
  })
```

```diff:src/routes/Todo/components/TodoView.js
 const TodoView = (props) => <div>
 -  foo: {props.foo}
 +  <ul style={{textAlign: "left"}}>
 +    {
 +      props.data.map((todo, i) => (
 +        <li key={i} style={{
 +          textDecoration: todo.isDone ? "line-through" : "none"
 +        }}>
 +          <input type="checkbox" checked={todo.isDone} /> 
 +          {todo.title}
 +        </li>
 +      ))
 +    }
 +  </ul>
  </div>
```

todo listっぽいものが表示できました：

<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/10487/bc81e09c-c0c9-0632-0de9-9d54d3c4e52f.png">

## 動的なものはsandboxのcontainer/reducerでモックを作る

しかし、checkboxは反応しないので、`onChange`を用意する必要があります。簡単なモックをこcontainer/reducerで作って、動作確認をします。

[詳細はこのコミット](https://github.com/konyavic/react-redux-firebase-todo/commit/65fcec81a51c7edda7e4624e2da98e86ab222861)

こんな感じで、ある程度動的な部分まで、`TodoView`のコンポネント単体を開発していけます。reducerの複雑なのロジックや、firebaseなど外部の状態と絡むものは、最後に`Todo`の方で繋ぎ込みます。

# `TodoView`とfirebaseの繋ぎ込み

sandboxで`TodoView`の実装ができたので、最後は`Todo`に戻ってfirebaseとの繋ぎ込みをやります。

 * 最初のデータを直接firebaseの管理画面で入力します：

<img width="583" alt="スクリーンショット 2016-12-01 21.39.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/10487/8211040c-6aa2-c961-33d3-aec113d99e2d.png">

 * Todoを修正します。[詳しくはこのコミット](https://github.com/konyavic/react-redux-firebase-todo/commit/c02865fb2f6b3d04be65ee15182c228163b9fcde)

`localhost:3000/todo`にアクセスすると、ちゃんとfirebaseから取得したデータが表示されました！

<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/10487/4902d402-338d-bfe1-6775-8fcb1afc4636.png">

そして、最後は：

 * [checkboxの`onChange`を繋ぎこむ](https://github.com/konyavic/react-redux-firebase-todo/commit/1fe56e6af2a5d5d5dc51581b1e8db179cf7f9c3d)
 * [新しいtodoを追加するための入力ボックスを作る](https://github.com/konyavic/react-redux-firebase-todo/commit/dd0c7600e591123a7cec71d0c50b6e700b61133f)

これで、todoの表示、追加、完了にするなど、todo listとして最低限の機能ができました！

<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/10487/c8a680cb-6350-fcd3-339e-46b77dcb205a.png">

と、ここにきて気づいたんですが、todoの削除を作るの忘れましたorz
加えて、元々のプランはもう一度`TodoView`に戻って、react-bootstrapなどでUIをブラッシュアップするつもりだったんですが、すでにボリューミーな記事になってしまったので、また次回で書いていきたいと思います。

# あとがき

firebaseのsecurity rulesを考える感覚は、今までのサービスの作り方とだいぶ違いますけど、簡単にreactなどのSPAフレームワークでserverlessなサービスを作れる世界になってきたので、今後も何か作ってみたいなーと思います。

----------
[^1]: https://github.com/davezuko/react-redux-starter-kit
[^2]: https://facebook.github.io/react/tutorial/tutorial.html
[^3]: https://firebase.google.com/docs/