もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

React(Gatsby)+ Firebaseでサーバレス入門した

個人開発で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>
  );
};

動かしてみるとこんな感じ

f:id:Rei19:20190608212724g:plain

おしまい

  • 簡単にしか触っていないけどFirestoreまわりの設計がキモになるなーという印象です。RDBでは○○できるのにとか思わずに(そもそも全然別物だけど)、Firestoreに最適化した設計を考えてかないといかんですね。
  • あとはCloudFunctionあたりを素振りすれば、だいたいサービス作るのに必要なものは事足りそう。
  • docment更新したときにonSnapshotで2回通知が流れてくるのがちょっと謎挙動でした。serverTimestampの仕様っぽいけど要調査。
  • テスト書くのはどうやるのかわかってないので後ほど

関連

というようなことを試してたらエンジニアHUBで気合の入った入門記事が流れてきたので紹介など

employment.en-japan.com