個人開発でFirebase使ってなんか作ろうかなということで、素振りで作ったものを公開してみました。Reduxのチュートリアルで作るTodoアプリをStoreをFirebaseにした体で作り変えたやつになります。Firebaseは古の時代に触ったときは単なるPub/SubできるDBだったのにいろいろ出来るようになっててビビりますね。触る前に公式のドキュメントをざっと読んでcodelabを試したくらいの事前知識で作りました。
作ったもの
サイトはこちら。単に最近遊んでるという理由だけでGatsby.jsで作ってます。Netlifyでホスティングしてますが、特にFirebase hostingを使ってない理由はありません。AuthとFirestoreだけ試したかったので。
gatsby-firebase-todo.netlify.com
コードはこちら
GitHub - rei-m/gatsby-firebase: Sample of Gatsby.js with Firebase
実装
Firebaseの設定
何はともあれエントリーポイントでfirebase.initializeApp
をしなければいけないのですが、Gatsbyはgatsby-browser.jsにonClientEntry
というAPIが生えているのでそこで行いました。これだけですね。
export const onClientEntry = () => { const config = { // your config }; firebase.initializeApp(config); };
Firebase.authentication
firebase.auth().onAuthStateChanged
にObserverを登録します。これはアプリケーション全体に影響するのでReact.Contextに認証状態を保持してアプリケーション全体を囲むようにしました。こんな感じのcustom hookを作って取れたuserオブジェクトをcontextで持つようにしてます。
useFirebaseAuth.ts
export const useFirebaseAuth = () => { const [user, setUser] = useState<User | null>(); useEffect(() => { const unsubscribe = firebase.auth().onAuthStateChanged(user => { if (user) { console.info(`firebase: authorized (uid: ${user.uid})`); const userName = user.displayName ? user.displayName : '名無し'; setUser({ uid: user.uid, name: userName }); } else { console.info(`firebase: unauthorized`); setUser(null); } }); return () => { console.info(`firebase: unsubscribe onAuthStateChanged`); unsubscribe(); }; }, []); return user; };
useEffectを使ってonAuthStateChangedのsubscribeを開始、componentが破棄されるときにunsbscribeするという感じになりますね。contextはこれだけになります。Gatsbyはgatsby-browserとgatsby-ssrのwrapRootElementでこのProbiderで包んであげればOKです。
FirebaseAuthProvider.tsx
export const FirebaseAuthContext = React.createContext<{ user?: User | null; }>({}); const FirebaseAuthProvider: React.FC<{}> = ({ children }) => ( <FirebaseAuthContext.Provider value={{ user: useFirebaseAuth() }}> {children} </FirebaseAuthContext.Provider> );
これで認証状態が変わったタイミングでcontextのuserが更新されて再描画が走るようになりました。contextの情報はuseContextを使えば参照できます。
Firebase.firestore
今回はよくあるTodoアプリを作るのでユーザーごとのTodoリストをFirestoreに保存します。構成は素朴にUsersというCorrectionの下にUser単位でdocmentを作り、その下にSubCorrectionでtodosを保存します。
FirestoreもonSnapshot
でObserverを登録する実装になる(リアルタイム同期が必要なければいらないけど)ので、同様にuseEffectを使ったcustom hookを作ります。
useFirestoreTodos.ts
export const useFirestoreTodos = (uid: string, filter: VisibilityFilter) => { const [todos, setTodos] = useState<Todo[]>(); useEffect(() => { const collection = todosCollection(uid); let query: firebase.firestore.Query; switch (filter) { case SHOW_ACTIVE: query = collection .where(`completed`, `==`, false) .orderBy(`createdAt`, `desc`); break; case SHOW_COMPLETED: query = collection .where(`completed`, `==`, true) .orderBy(`createdAt`, `desc`); break; default: query = collection.orderBy(`createdAt`, `desc`); break; } const unsubscribe = query.onSnapshot(snapshot => { console.info(`firestore: receive todos: size=${snapshot.docs.length}`); const todos = snapshot.docs.map(doc => toModel(doc.id, doc.data())); setTodos(todos); }); return () => { console.info(`firestore: unsubscribe onSnapshot:todos`); unsubscribe(); }; }, [filter]); return todos; };
authと同様ですが、リストの検索条件は変更出来るので、useEffectの第2引数にfilterを指定してfilterが変更されたらobserverを登録し直すようにします。実際にこのhookを使ったcomponentはこんな形になります。
TodoContents.tsc
const TodoContents = ({ user }: Props) => { const [filter, setFilter] = useState<VisibilityFilter>(SHOW_ALL); const todos = useFirestoreTodos(user.uid, filter); const handleAddTodoSubmit = async (todoName: string) => { await addTodoAction(user.uid, todoName); }; const handleClickTodo = async (todo: Todo) => { await updateTodoAction(user.uid, todo.id, !todo.completed); }; const handleClickDeleteTodo = async (todo: Todo) => { await deleteTodoAction(user.uid, todo.id); }; return ( <section> {todos ? ( <> <AddTodoForm onSubmit={handleAddTodoSubmit} /> <TodoList todos={todos} onClickTodo={handleClickTodo} onClickDeleteTodo={handleClickDeleteTodo} /> <TodoFilter currentFilter={filter} onClick={setFilter} /> </> ) : ( <div>ろーでいんぐ</div> )} </section> ); };
動かしてみるとこんな感じ
おしまい
- 簡単にしか触っていないけどFirestoreまわりの設計がキモになるなーという印象です。RDBでは○○できるのにとか思わずに(そもそも全然別物だけど)、Firestoreに最適化した設計を考えてかないといかんですね。
- あとはCloudFunctionあたりを素振りすれば、だいたいサービス作るのに必要なものは事足りそう。
- docment更新したときにonSnapshotで2回通知が流れてくるのがちょっと謎挙動でした。serverTimestampの仕様っぽいけど要調査。
- テスト書くのはどうやるのかわかってないので後ほど
関連
というようなことを試してたらエンジニアHUBで気合の入った入門記事が流れてきたので紹介など