1年ほどFirebase x TypeScriptで開発してきて定まってきた僕なりの書き方



# はじめに
去年(2018)春ごろから、`Firebase` x `React` x `TypeScript`でアプリケーションを作成する機会が多くなり、
自分の中である程度書き方が固まってきたので、その内容をまとめることにしました。

**フロントエンドの実装を想定した説明をしますが、サーバーサイドも似たような構造で筆者は書きます**。
`Firebase`、`TypeScript`を使ったことがないかたは、少し難しいかもしれません。

「俺はこのような書き方してるよ〜」などあれば、コメントで教えて欲しいです！

# アーキテクチャについて
筆者は`Firebase`プロジェクトでの設計では、**レポジトリパターンを使って設計することが多い**です。
※_厳密にレポジトリパターンになっているかといわれると、なっていないかも..._

下記がフォルダ構造です。

```
src
├ entities
├ components
├ repositories
└ services
```

### 各層の責務と依存関係について

##### entities

使用するデータの型を定義したファイルを保存します。
各エンティティファイルで**ライブラリ以外の依存はさせないように疎結合に実装する**。


```
src
├ entities
| ├ User.ts
| └ Post.ts
├ components
├ repositories
└ services
```

##### components

使用するコンポーネントファイルを保存します。
**`Atomic Design`に基づいた切り方、依存関係でコンポーネントを作成する**。

※_詳しくは`Atomic Design`で調べて下さい。_

##### repositories

DB(主に`Firestore`)に保存されているデータの読み込み・書き込みのコードをレポジトリ内で隠蔽できるように書く。
各レポジトリファイルで**`entitiles`もしくはライブラリ以外の依存はさせないように実装する**。

```
src
├ entities
├ components
├ repositories
| ├ user.ts
| └ post.ts
└ services
```

##### services

日付変換やカスタムフック(`Hooks`)、`Firestore`のスナップショットのリッスンなどを書く。
`repositories`だけでは再現できない複雑なロジックを含んだ関数などを定義するイメージです。
各サービスファイルは**他のサービスファイル、`entities`、そしてライブラリ全てに対して依存しても良いです。**

※_腐敗防止的な役割もになっています。_

```
src
├ entities
├ components
├ repositories
└ services
  ├ user.ts
  └ post.ts
```

### firestore周りの実装について
`Firestore`とのやり取りは基本的に`repositories`内で閉じさせるように実装しますが、
例外として、**リアルタイムにデータを取得するために使用するスナップショットのリッスン(チャット実装などで必須)は`servicesにカスタムフック`として書きます**。

リッスンを`repositories`に書かない理由は、「**`repositories`を`get`と`set`のシンプルな形で実現させたい**」「**リッスンは`DB`の読み込みではなく、リアルタイムに保存されているデータの変更を検知するいちサービスとして捉えることにした**」です。

ここら辺は、賛否両論あると思うのでコメントにて色々指摘欲しいです。

# 各層の書き方

### entitiesの書き方
`entities`には、保存するモデルの型と型に沿ったデータ変換を主に書くことになります。
エンティティの書き方を見る前に、`Firestore`のデータ設計について軽く触れます。

`firestore`はコレクションとドキュメントの階層構造になっているので、
**ドキュメントにモデル情報を保存し、コレクションで複数のモデルをまとめるような設計になる**。

下記はUserモデルを保存する場合の構造例です。

```
Users(collection)
├ kurino(document)
├ satou(document)
├ tarou(document)
└ hanako(document)
```

上記の構造からわかるように、**`document`に対して型を添えると相性がいい**です。

`User`の型を定義し利用することで、`kurino`, `satou`, ... は型にハマったデータが保存されていることになるので扱いやすくなります。

**また型をかませるたデータに変換する関数を用意しておくと、`Firestore`のデータ読み込み時に便利です**。

下記は`User`の型を定義する例です。

```

src
├ entities
| └ User.ts // ここ
├ components
├ repositories
└ services
```

```ts
export type User = {
  name: string
  age?: number
  gender: 'male' | 'female' | 'other'
}

export type UpdateUser = {
  name?: string
  age?: number
  gender?: 'male' | 'female' | 'other'
}

export const buildUser = (data: firebase.firestore.DocumentData) => {
  const user: User = {
    name: data.name
    age: data.age
    gender: data.gender
  }

  return user
}
```

**`entities`にデータの型を定義することで、
どのようなデータが`Firestore`に保存されているのか、そしてどのようなデータを保存していいのか明確になります**。

### componentsの書き方
こちらは、`TypeScript`の書き方というよりも、コンポーネントの粒度をどうするかの話になってくるので説明しません。とりあえず、**`Atomic Design`を学びましょう**。

### repositoriesの書き方
`repositories`には、`entities`で定義した型の**モデルデータ(`document`)の読み込みと書き込み**を実装します。

基本的に、`createXXXX`, `updateXXXX`, `getXXXX`, `setXXXX`の名前の関数が並びます。

下記は`User`のデータ読み込み・書き込みの例です。

```
src
├ entities
| └ User.ts
├ components
├ repositories
| └ user.ts // ここ
└ services
```

```ts
const db = firebase.firestore()
const usersRef = db.collection('users')

export const getUser = (id: string) => {
  try {
    const snapshot = await usersRef.doc(id).get()
    const user = buildUser(snapshot.data()) // entitiesで定義した変換関数
    return user
  } catch (e) {
    console.warn(e)
    return null
  }
}

// UpdateUserはentitiesで定義した型
export const setUser = (id: string, user: UpdateUser ) => {
  try {
    const batch = db.batch()

    batch.set(
      usersRef.doc(uid),
      {
        ...(user.name && { name: user.name }),
        ...(user.age && { age: user.age }),
        ...(user.gender && { gender: user.gender })
      },
      { merge: true }
    )

    await batch.commit()

    return { result: true }
  } catch (e) {
    console.warn(e)
    return { result: false }
  }
}
```

**`repositories`にデータ参照・編集の役割を漏れ出さないように実装することで、
`DB`周りの操作においてどのように実装しようか迷うことがなくなり、保守・拡張もしやすくなります**。


### servicesの書き方
`services`には、`entities`や`repositories`などを利用して、**実際に`components`や画面で使用する関数やカスタムフック**を実装します。

下記はチャットを実装する場合の例で、`User`に紐づく`messages`のスナップショットのリッスンです。

※本来は`rooms`など区切って実装しますが、簡略化する為そこらへんは無視して実装しています。

```
src
├ entities
| └ User.ts
├ components
├ repositories
| └ user.ts
└ services
  └ chat.ts // ここ
```

```ts
const db = firebase.firestore()
const usersRef = db.collection('users')

export const useChat = (userID: string) => {
  const messagesRef = usersRef.doc(userID).collection('messages').orderBy('createdAt', 'desc')
  const [messages, setMessages] = useState<Message[]>() // entitiesにMessageが定義されている想定。

  useEffect(() => {
    const unsubscribe = messagesRef.onSnapshot({
      next: (snapshot: firebase.firestore.QuerySnapshot) => {
        const messages = snapshot.docs
          .map(doc => {
            const message = buildMessage(doc.id, doc.data()) // repositoriesにbuildMessageがある想定。
            return message
          })
        setMessages(messages)
      },
      error: (error: Error) => {
        console.warn(error)
      }
    })

    return () => {
      unsubscribe()
    }
  }, [messagesRef])

  const onSend = useCallback((text: string) => {
    createMessage(text) // repositoriesにcreateMessageがある想定。
  }, [])

  return { messages, onSend }
}
```

下記はモーダル開閉を管理するカスタムフックの例です。

```cmd
src
├ entities
| └ User.ts
├ components
├ repositories
| └ user.ts
└ services
  ├ chat.ts
  └ modal.ts // ここ
```

```ts
export const useModal = () => {
  const [isVisible, setIsVisible] = useState<boolean>(false)

  const onOpen = useCallback(() => {
    setIsVisible(true)
  }, [])

  const onClose = useCallback(() => {
    setIsVisible(false)
  }, [])

  return { isVisible, onOpen, onClose }
}
```

**`services`に実際に`components`で利用する関数を書くことで、
複雑なロジックを含まないシンプルな`repositories`と、見た目にのみ徹した`components`の実現を可能にし、再利用可能な関数をまとめあげることができます**。

# おまけ(Tips)

### timestampを上手に扱う
基本的に何もなければ、**`createdAt`, `updatedAt`を`document`に保存する**と思います。
その時、`entities`に`Document.ts`を作っておくと便利です。

```ts
export const createDocument = <T>(document: T) => {
  return {
    ...document,
    createdAt: firebase.firestore.FieldValue.serverTimestamp(),
    updatedAt: firebase.firestore.FieldValue.serverTimestamp()
  }
}

export const updateDocument = <T>(document: T) => {
  return {
    ...document,
    updatedAt: firebase.firestore.FieldValue.serverTimestamp()
  }
}
```

上記のエンティティ関数を使って、本編で定義した`repositories`を書き換えると...

```ts
const db = firebase.firestore()
const usersRef = db.collection('users')

export const setUser = (id: string, user: UpdateUser) => {
  try {
    const batch = db.batch()

    batch.set(
      usersRef.doc(uid),updateDocument<UpdateUser>({
        ...(user.name && { name: user.name }),
        ...(user.age && { age: user.age }),
        ...(user.gender && { gender: user.gender })
      }),
      { merge: true }
    )

    await batch.commit()

    return { result: true }
  } catch (e) {
    console.warn(e)
    return { result: false }
  }
}
```

### ステートのグローバル管理(ContextAPI, Redux)
`React`標準搭載の`ContextAPI`、王道と名高い`Redux`どちらにしろ**別フォルダを切ってコードを書いた方がすっきりします**。

下記が`user`をグローバル管理したときのフォルダ構造です。

```
src
├ entities
├ components
├ repositories
├ services
└ store
  ├ user
  : ├ actions
    ├ state
    └ reducers
```

気を付けることは、`repositories`を汚さないことを意識する。
つまり、**`services`と`components`からのみグローバルステートの`Dispatch`を行う**。

# さいごに
`TypeScript`を使用した全体の具体的な設計の話をしている記事が少ないと感じたので、今回自分なりに書かせて頂きました。これは**筆者が、1年ほど`Firebase` x `React` x `TypeScript`でアプリケーションを作成して固まった設計なのでまだまだたくさん改善の予知はあると思います**。これから`Firebase` x `React` x `TypeScript`でアプリケーションを作成することがあるとき少しでも助けになれれば嬉しいです。

最近活発にコミットを積んでいる`React Native`で書かれたレポジトリを載せておきます。
実際に、これまで説明した設計で実装を進めていますので、参考になるかもしれません。

https://github.com/kousaku-maron/party

最後までお読みいただきありがとうございました！
Thank you !