1. acro5piano

    No comment

    acro5piano
Changes in body
Source | HTML | Preview
@@ -1,194 +1,194 @@
「辛かった」と書いていますが、実際にはまだ「辛い」です。
Apollo の導入を検討している方の参考になれば幸いです。
# 概要
- React Native + TypeScript のプロジェクトで Apollo Client を使ってみた
- すげー便利!って感じで最初は始まった
-- だんだん、 Apollo のキャッシュが効いてされて、「画面更新されない」問題にぶち当たる
+- だんだん、 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歳です。