75
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-06-06

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

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

概要

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

Apollo Client とは

image.png

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

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

Apollo Client の特徴

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

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

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 で苦しんだのは、共通ロジックです。

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

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

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

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

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

すげー便利

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

また、他にも

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

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

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

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

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

どうしたか

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

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

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

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

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

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

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

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歳です。

75
45
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
75
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?