作ったやつ
川柳投稿するだけのサービス
目的
毎年なにかしら新しい言語とか技術を覚えてアウトプットするというのを続けていて、今年はあまり使ったことがなかった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と一蓮托生する形で設計した方がいいかもわからんです。