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

  • 6
    いいね
  • 0
    コメント

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

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

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

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

react-redux-starter-kitでfirebaseを利用する

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

react-redux-starter-kit

まずは、react-redux-starter-kit1をcloneして、npm installnpm startすれば、localhost:3000にて下記のアヒルが現れます:

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.jsTodoRouteを追加する

詳しくはこちらのコミット

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

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>
  )

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

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は慎重に決める必要があります)

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の修正を一部省きます)

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>
 +  }
 +}
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も一旦認証なしに切り替えます:

(危険なので、あとで必ず戻しましょう)

スクリーンショット 2016-12-03 10.31.10.png

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

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

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

ここまでの作業はこのコミット

firebaseの認証を通す

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

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

詳しくはこのコミット

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

スクリーンショット 2016-12-01 18.20.17.png

TodoViewを作り込む

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

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に追加します

詳しくはこのコミット

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

静的なTodoViewを作り込む

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

src/routes/Sandbox/containers/TodoViewContainer.js
  const mapStateToProps = (state) => ({
 -  foo: 123
 +  data: [
 +    {
 +      title: "仕事する",
 +      isDone: false,
 +    },
 +    {
 +      title: "アドベントカレンダーを書く",
 +      isDone: false,
 +    },
 +    {
 +      title: "寝る",
 +      isDone: false,
 +    },
 +  ]
  })
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っぽいものが表示できました:

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

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

詳細はこのコミット

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

TodoViewとfirebaseの繋ぎ込み

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

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

スクリーンショット 2016-12-01 21.39.09.png

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

そして、最後は:

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

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

あとがき

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


この投稿は KLab Advent Calendar 20163日目の記事です。