Apollo Client + React Native + TypeScript で辛かった話

「辛かった」と書いていますが、実際にはまだ「辛い」です。
Apollo の導入を検討している方の参考になれば幸いです。

[追記 2021年4月]
本記事を書いた時点(2019年9月)では、私の Apollo Client の理解が浅かったのが原因でした。コメントで仰って下さっている通り `queryWrite` 等のキャッシュをちゃんと設定すれば、大半のケースでは問題が無いと思います。
ただ、 Apollo Client で辛いと感じている方がいるのは事実で、特にキャッシュの部分は[こちらの記事](https://qiita.com/taneba/items/ae5d2ff6946e751dcadf)でも言及されていました。
個人的には、現在は URQL で `additionalTypenames` をちゃんと使ってます。 Refetch を自動的に行ってくれるので最高です。
[/追記]

# 概要

- React Native + TypeScript のプロジェクトで Apollo Client を使ってみた
- すげー便利！って感じで最初は始まった
- だんだん、 Apollo のキャッシュが効きすぎて、「画面更新されない」問題にぶち当たる
- そのうち、 Apollo のキャッシュを無効化する作業に入る
- Apollo 使ってる意味なくね、ってなる
- `MobX` と `fetch` ベースのライブラリ `ky` ベースで、全部書き換える ~~（進行形）~~
- (2021年4月) URQL を愛用

# Apollo Client とは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103885/b960fbb2-d54c-3434-307d-d3ed834ef98a.png)

https://www.apollographql.com/

Apollo Client は、 Apollo という GraphQL 界の重鎮が作っているフレームワークのクライアントライブラリです。

Apollo Server や Apollo CLI などもありますが、 Apollo Client は React などの JavaScript ライブラリと組み合わせて使うライブラリになります。

# Apollo Client の特徴

- コンポーネントが **マウントされた時に、データ取得が走る** （ここ重要）
- `__typename` と `id` によるキャッシュにより、無駄なデータの取得をしない
    - フィールドが追加された場合は、再度取得する
    - クエリ時に `fetchPolicy: 'no-cache'` などとすると、強制的に再取得する
- ミドルウェア的なものをかませて、エラー時の処理などを追加できる
- Redux や MobX などに代わる、 `apollo-link-state` というライブラリと組み合わせると、最強（になるはず）

具体的には、こんな感じです。

```tsx:UserDetail.tsx
import { graphql } from 'react-apollo'
import gql from 'grpahql-tag'
import { ActivityIndicator, View, Text } from 'react-native'

const GET_USER = gql`
  query getUser {
    user {
      id
      name
    }
  }
`

// 実際には、 $WithGraphQL<User> みたいにして DRY にする
interface WithUser {
  data: {
    user: {
      id: number
      name: string
      birthday: string
    }
  }
}


export function UserDetail({ data }: WithUser) {
  if (data.loading) { 
    return <ActivityIndicator />
  }
  return (
    <View>
      <Text>{data.user.id}</Text>
      <Text>{data.user.name}</Text>
    </View>
  ) 
}

export default grpahql(GET_USER)(UserDetail)
```

# 辛くなっていく過程

## 最初

ブラウザの React で使ったことがあり、今回の React Native のプロジェクトでも使うことにしました。
ただ、ブラウザの React で苦しんだのは、共通ロジックです。

コンポーネント間で共有したいロジックがある場合（例: ユーザーの生年月日をパースして、年齢を出す）、コンポーネントにべた書きすると、ロジックが各コンポーネントに分散するので、微妙です。

そこで、下記のようなファイルを作成してまとめれば、いける、と判断しました。

```typescript:entities/User.ts
export function getAge(user: User) {
  //
}
```

Apollo でシリアライゼーション層みたいなのができればいいんですけどね。結構調べたけど、無理っぽいですね。

結果的に、これは、何とかなりました。実際には、 `Partial<User, 'birthday'>` などとして、汎用性を高めています。

https://github.com/apollographql/apollo-tooling#apollo-clientcodegen-output

## すげー便利

上記の書き方を採用すると、コンポーネントはちょっと厚くなりますが、全体的なコード量が減るので、便利になります。
特に Redux 等のライブラリで状態管理をするよりも、格段にコード量は減ります。

また、 `apollo-codegen` という神ツールを使うと、 GraphQL から TypeScript の型定義を作成できるので、工数がかなり減らせます。静的な GraphQL のチェックにもなる。 CI で回せばなお良い。
これは今でも使っています。

## Apollo のキャッシュ に苦しみ始める

キャッシュ。これが難しい。

単純なユーザー情報の更新を考えます。

- `Show` コンポーネントで、ユーザーが自身の情報を見る
- `Edit` コンポーネントで、ユーザーが Update 系の行動をする (`mutation`）
- `Show` コンポーネントで、もう一度自身の情報を見る

これは比較的簡単です。 `Edit` コンポーネントで `mutate` する時に、ユーザー情報を全て fetch すれば良いからです。
ただ、これだけでも、

- 取得フィールドを `Show` と `Edit` でほぼ完全に一致させる
- mutation 側のクエリで `fetchPolicy: 'no-cache'`

という注意点があります。（これエラー出ないから、気づかないんだよな・・・）

次に、もっと複雑な状態変化を考えます。ちょっと具体的です。

- `Show` コンポーネントで、ユーザーが自身の情報「と」閲覧可能な動画が表示される
    - この時、ユーザーは15歳で登録している、とする
- `Edit` コンポーネントで、ユーザーが生年月日を変えて、30歳になる
- サーバーサイドで、ユーザーの別の状態が更新される
    - 例: 生年月日を変えたら、エロ動画も表示できるようになる
- `Show` コンポーネントで、ユーザーが自身の情報「と」閲覧可能な動画が表示される
    - ここで、エロ動画を表示させたい！

※エロ動画はただの例です。

この時、動画を取得するクエリを **再度実行(refetch)** しないといけないですが、 React Native + Apollo だとこれが難しい。

理由としては、同じ React でも、ブラウザとスマホアプリの違いとして、ブラウザでは「画面リロード」という最終手段があるのに対し、スマホアプリでは、アプリを落とさないといけない、というのがあります。

対策として、強制的にキャッシュを無効化し始めます。

```tsx
graphql(GET_USER_AND_MOVIES, {
  fetchPolicy: 'no-cache',
})(UserDetail)
```

一見これで解決しそうです。実際、ブラウザでは、画面変更により再度コンポーネントがマウントされるので、GraphQLが再度実行され、問題ありません。

が。アプリの場合、 `componentDidMount` が、アプリでは Stack を重ねるだけなので発火しないことがあります。

React Navigation というライブラリでルーティングを管理していたのですが、最終的に、 下記のようなコードを随所に書き始めるようになりました。

```tsx
componentDidMount() {
  const { data, navigation } = this.props
  navigation.addListner('willFocus', () => data.refetch())
}
```

ただし、こうしても、ナビゲーションの種類（ `TabNavigation` なのか、 `StackNavigation` なのか、更にその親子関係）によって挙動が代わるので、それも意識しながらプログラミングする必要が出てきます。

また、他にも

- `refetch` を行った時は `data.loading` が `false` になる
    - そのためには `notifyNetworkStatus: true` オプションを指定する
    - でも指定しても `false` のままになる（ことがある？）
- `refetch` 時に、関連コンポーネントの状態が更新されるので、画面の後ろ側に出ているコンポーネント側でエラーになったりする
    - デバッグがしんどい

など、マイナーだけど重要な仕様に悩まされました。
コンポーネントがエラーになったり、再取得時にローディングが出せない。
せっかく年齢を偽って、エロ動画が見れるようになったのに、15歳の少年はそれに気づかない可能性がある。

これでは運営も少年も、悶々とした日々を送ることになってしまいます。

対策としては、年齢を変更した時に、 Redux 等で別でステート管理し、そちらで GraphQL リクエストを発火させ、キャッシュを更新させる、という方法が思いつきます。
が、あまり直感的ではないコードになりそうです。

Apollo のキャッシュは優秀ですが、 React Native では致命傷を負いかねない、という結論になりました。

# どうしたか

react-apollo を捨てて、 `MobX` に置き換えました。

Redux でも何でも良いんですが、 `mapStateToProps` 等の記述量と TypeScript との相性を考えた結果です。

MobX で記述すると、下記のメリットがありました。

- キャッシュ削除の処理をストア側で一元管理できる
- ストアに状態操作ロジックが集中するので、コンポーネントが薄くなる
- MobX ではストアはただのオブジェクトなので、テストが書きやすい

また、未だに `apollo-client` というライブラリで GraphQL を取得していますが、これも `ky` というライブラリでやろうと思っています。

https://github.com/sindresorhus/ky

これは Apollo から距離を置きたい気持ちがあります。

が、一旦、下記のような関数を使ってリクエストしています。

```ts
export const apolloClient = new ApolloClient({
  link: ApolloLink.from(links),
  cache,
})

export function query<T, V extends {} = {}>(query: any, variables?: V) {
  return apolloClient.query<T, V>({
    fetchPolicy: 'no-cache',
    query,
    variables,
  })
}

export function mutate<T, V extends {} = {}>(mutation: any, variables?: V) {
  return apolloClient.mutate<T, V>({
    fetchPolicy: 'no-cache',
    mutation,
    variables,
  })
}
```

# 考察

react-apollo はデータ・ドリブンなコンポーネントが作れて良いのですが、アプリ全体に関わるようなロジックは、 Apollo で管理すると荷が重いのではないか、と考えています。
もっと上手くキャッシュを扱う方法があるとは思いますが、現時点での私の能力では太刀打ち出来ないものでした。

現在も react-apollo は使っていますが、下記のようなユースケースに絞っています。

- アプリ内で変更が滅多に起こらない部分
    - 例: カテゴリ一覧。素人、熟女、清楚、ロリ、など
- データがコンポーネント内に完全に閉じている部分
    - 例: お気にいりのエロ動画。追加と削除しかなく、他の部分に影響を与えない

以上です。ちなみに私は26歳です。