こんな感じの、よくある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 install
、npm 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.js
にTodoRoute
を追加する
ついでに、starter-kitのヘッダーにリンクを追加しておきます:
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は慎重に決める必要があります)
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の修正を一部省きます)
+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>
+ }
+}
+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も一旦認証なしに切り替えます:
(危険なので、あとで必ず戻しましょう)
これで、もの自体はもう動きますが、webpack的には共通ライブラリーをvendor.js
にまとめてダウンロードするので、firebaseも一緒に入れましょう:
compiler_vendors : [
'react',
'react-redux',
'react-router',
- 'redux'
+ 'redux',
+ 'firebase'
],
さて、localhost:3000/todo
にアクセスしてみましょう。書き込んだbar
がちゃんと反映されました:
firebaseの認証を通す
認証なしのsecurity rulesだと悪用される可能性があるし、ユーザ毎に状態を永続化したいので、ここでちゃんとfirebaseの認証を通すように改修します:
- 認証通っていないときは、認証のボタンを表示します
- そのボタンの
onClick
でfirebase.auth().signInWithPopup
を呼び出します -
componentDidMount
のタイミングでfirebase.auth().onAuthStateChanged
のコールバックを設定します - ちなみに、
onAuthStateChanged
のコールバックは認証が通る前に必ず一度呼ばれるので、呼ばれたから認証が通ったわけではありません(戻り値のuserはnull)
これで、一回目のアクセスは下記のように「auth」と書いてるボタンが表示され、そこをクリックすると認証ポップアップが出てきます。認証が通ったら、props.isAuth
が更新されるので、画面は前のfoo: bar
に切り替わります:
TodoView
を作り込む
これでやっと、react+firebaseの土台が完成しました。さて、ここからtodo listの中身を作っていきたいので、TodoView
というコンポネントに分割して作業します:
+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
にアクセスして、表示を確認します:
静的なTodoView
を作り込む
これで、view以外のもの(いわゆるmodule/controller)に影響されない足場ができたので、TodoView
の設計をどんどん作り込んでいきます。まずはtodoのデータを静的に書いて、sandbox/todoview
で表示を確認します:
const mapStateToProps = (state) => ({
- foo: 123
+ data: [
+ {
+ title: "仕事する",
+ isDone: false,
+ },
+ {
+ title: "アドベントカレンダーを書く",
+ isDone: false,
+ },
+ {
+ title: "寝る",
+ isDone: false,
+ },
+ ]
})
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の管理画面で入力します:
- Todoを修正します。詳しくはこのコミット
localhost:3000/todo
にアクセスすると、ちゃんとfirebaseから取得したデータが表示されました!
そして、最後は:
これで、todoの表示、追加、完了にするなど、todo listとして最低限の機能ができました!
と、ここにきて気づいたんですが、todoの削除を作るの忘れましたorz
加えて、元々のプランはもう一度TodoView
に戻って、react-bootstrapなどでUIをブラッシュアップするつもりだったんですが、すでにボリューミーな記事になってしまったので、また次回で書いていきたいと思います。
あとがき
firebaseのsecurity rulesを考える感覚は、今までのサービスの作り方とだいぶ違いますけど、簡単にreactなどのSPAフレームワークでserverlessなサービスを作れる世界になってきたので、今後も何か作ってみたいなーと思います。