1. acro5piano

    Posted

    acro5piano
Changes in title
+Apollo Client + React Native + TypeScript で辛かった話
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,194 @@
+「辛かった」と書いていますが、実際にはまだ「辛い」です。
+Apollo の導入を検討している方の参考になれば幸いです。
+
+# 概要
+
+- React Native + TypeScript のプロジェクトで Apollo Client を使ってみた
+- すげー便利!って感じで最初は始まった
+- だんだん、 Apollo のキャッシュが効いてされて、「画面更新されない」問題にぶち当たる
+- そのうち、 Apollo のキャッシュを無効化する作業に入る
+- Apollo 使ってる意味なくね、ってなる
+- `fetch` ベースのライブラリ `ky` ベースで、全部書き換える(進行形)
+
+# 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'
+
+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 でシリアライゼーション層みたいなのができればいいんですけどね。結構調べたけど、無理っぽいですね。
+
+## すげー便利
+
+コンポーネントはちょっと暑くなりますが、全体的なコード量が減るので、便利になります。
+特に Redux 等のライブラリで状態管理をするよりも、格段にコード量は減ります。
+
+また、 `apollo-codegen` という神ツールを使うと、GraphQLからTypeScriptの型定義もできるので、工数がかなり減らせます。静的なチェックにもなる。CIで回せば最強。
+これは今でも使っています。
+
+## Apollo のキャッシュ に苦しみ始める
+
+キャッシュ。これが難しい。
+
+単純なユーザー情報の更新を考えます。
+
+
+これは比較的簡単です。 `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())
+}
+```
+
+また、他にも
+
+- `refetch` を行った時は `data.loading` が `false` になる
+ - そのためには `notifyNetworkStatus: true` オプションを指定する
+ - でも指定しても `false` のままになる(ことがある?)
+- `refetch` 時に、関連コンポーネントの状態が更新されるので、画面の後ろ側に出ているコンポーネント側でエラーになったりする
+ - デバッグがしんどい
+
+など、マイナーだけど重要な仕様に悩まされました。
+コンポーネントがエラーになったり、再取得時にローディングが出せない。
+せっかく年齢を偽って、エロ動画が見れるようになったのに、15歳の少年はそれに気づかない可能性がある。
+
+これでは運営も少年も、悶々とした日々を送ることになってしまいます。
+
+対策としては、年齢を変更した時に、 Redux 等で別でステート管理し、そちらで GraphQL リクエストを発火させ、キャッシュを更新させる、という方法が思いつきます。
+が、あまり直感的ではないコードになりそうです。
+
+Apollo のキャッシュは優秀ですが、 React Native では致命傷を負いかねない、という結論になりました。
+
+# どうしたか
+
+react-apollo を捨てて、 `MobX` に置き換えました。
+
+Redux でも何でも良いんですが、 `mapStateToProps` 等の記述量と TypeScript との相性を考えた結果です。
+
+MobX で記述すると、下記のメリットがありました。
+
+- キャッシュ削除の処理をストア側で一元管理できる
+- ストアに状態操作ロジックが集中するので、コンポーネントが薄くなる
+- MobX ではストアはただのオブジェクトなので、テストが書きやすい
+
+# 考察
+
+react-apollo はデータ・ドリブンなコンポーネントが作れて良いのですが、アプリ全体に関わるようなロジックは、 Apollo で管理すると荷が重いのではないか。
+
+現在も react-apollo は使っていますが、下記のようなユースケースです。
+
+- アプリ内で変更が滅多に起こらない部分
+ - 例: カテゴリ一覧。素人、熟女、清楚、ロリ、など
+- データがコンポーネント内に完全に閉じている部分
+ - 例: お気にいりのエロ動画。追加と削除しかなく、他の部分に影響を与えない
+
+以上です。ちなみに私は26歳です。