もやもやエンジニア

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

React(Gatsby)+ FirebaseでWebサービス作った

作ったやつ

川柳投稿するだけのサービス

senryu.app

目的

毎年なにかしら新しい言語とか技術を覚えてアウトプットするというのを続けていて、今年はあまり使ったことがなかったFirebaseメインでなにか作ってみるかということで、認証+コンテンツのCRUDがあるスタンダードなWebサービスを作りました。今回はFirebaseを使うというのが目的なので、使ってもらえそうかどうかというのはあまり考えずに作ってます。よく新しい技術覚えるときにTODOリスト作ったりしますが、簡単なサンプル作っただけだと出来た気になるだけであんまりよくないと思っているので、ちゃんとそれっぽいサービスを作るようにしてます。

技術的な話

Firebaseで○○を作ってみた系の記事はたくさんあるので、あまり言及することもないのですが、設計的なところだけ少し。Presentation層はできるだけFirebaseの存在を意識しないように作っています(ただFirebaseのコンテキストにおいてはこの方針がベターかはわからないです。FirebaseUIを使っている関係上、認証部分にはFirebaseが漏れていたりするので)。具体的なコードのイメージは以下のとおりです。

ドメイン層を作ってリポジトリのIFを配置

src/domain/repositories/index.ts

export interface SenryuRepository {
  findById(id: SenryuId): Promise<Senryu>;
  findByUserPerPage(
    userId: UserId,
    pageNo: number,
    base?: Senryu
  ): Promise<Page<Senryu>>;
  findAllPerPage(pageNo: number, base?: Senryu): Promise<Page<Senryu>>;
  add(senryu: SenryuDraft): Promise<SenryuId>;
  delete(senryuId: SenryuId): Promise<void>;
}

インフラ層でリポジトリを実装

src/infrastructure/repositories/SenryuRepositoryData.ts

export class SenryuRepositoryData implements SenryuRepository {
  async findById(id: SenryuId) {
    try {
      const snap = await senryuCollection()
        .doc(id)
        .get();
      const data = snap.data();
      if (data !== undefined) {
        return dataToModel(id, data);
      } else {
        throw reasonToAppError({ code: 'not-found' }, '川柳');
      }
    } catch (reason) {
      throw reasonToAppError(reason, '川柳');
    }
  }

  // 中略
}

ReactのContextを使ってRepositoryの実装をDIできるようにする

src/contexts/DiContainerContextProvider.tsx

export const defalutValue = {
  senryuRepository: new SenryuRepositoryData(), // ここで実体を作成
};

export const DiContainerContext = React.createContext<{
  senryuRepository: SenryuRepository;
}>(defalutValue);

const DiContainerContextProvider: React.FC<{}> = ({ children }) => (
  <DiContainerContext.Provider value={defalutValue}>
    {children}
  </DiContainerContext.Provider>
);

src/hooks/useDiContainer.ts

import { useContext } from 'react';
import { DiContainerContext } from '@src/contexts/DiContainerContextProvider';

export const useDiContainer = () => useContext(DiContainerContext);

custom hookでContextからRepositoryをDIする

src/hooks/useSenryuList.ts

type Deps = {
  senryuRepository: SenryuRepository;
};

export const useSenryuList = (
  { senryuRepository }: Deps = useDiContainer()
): Return => {
  // 中略
}

hookのテストはRepositoryのモックで差し替えることができる

const genMockSenryuRepository = () =>
  jest.genMockFromModule<SenryuRepository>('@src/domain/repositories');

const senryuRepository = genMockSenryuRepository();
senryuRepository.findAllPerPage = jest.fn(pageNo => {
   // テスト用のデータをPromiseで返す
});

const { result, waitForNextUpdate } = renderHook(() =>
    useSenryuList({ senryuRepository })
);

という感じでFirebaseはRepositoryの実装に封印する形で作っています。これは普段RESTなAPIを使うときにやっている実装で、そこをFirebaseに置き換えた体ですね。とはいえ先にもコメントしたとおり、Firebase使う場合はこの形がよいのかはまだわかってないので、Firebaseと一蓮托生する形で設計した方がいいかもわからんです。

おしまい

  • Gatsbyである必要全く無いのですが、最近遊んでたというだけの理由で使ってます。
  • 大きな残件としてFirebaseにからむところのテストの書き方を調べきれてないので、そのあたりまでカバーして、デザインとかSEOまわりを見直したらいったんこれの開発は終わりにする予定
  • ドメインを初めてGoogle Domainsで取ったのですが、普段使ってるお名前.comよりだいぶ使いやすかったです。
  • いつも通りコードはポートフォリオ代わりに公開してます。