TypeScript+react+apollo/client でSubscriptionを実現する

4 min read読了の目安(約4000字

GraphQLを利用したSubscriptionを試してみようと思いHOW TO GRAPHQL(https://www.howtographql.com/)をベースに触ってみたものの、ClientをTypeScriptにしたことで思った以上にはまったことと、この組み合わせに関する記述が検索に引っかからなかったので、備忘録として記録します。

やろうとした処理

初期処理としてQueryにより一覧を取得し、Subscriptionを用いて更新・追加が発生したものを一覧に反映させる。

結論

useSubscription hookを利用して onSubscribe に更新データを受領した際の処理を記述します。

この記事でのデータ構成

HOW TO GRAPHQLの例に対して少し省略して以下のような構成にしています。

  • Link:id,url,descriptionの情報を持つ型
  • feed:一覧としてLinkの配列を格納する

定義は以下のような形にしています。

type Link = {
  id: string | undefined;
  url: string | undefined;
  description: string | undefined;
};

interface FeedResult {
  feed: Link[];
}

Subscribeの手法

以下の2つの方法があります。

  1. useQueryの返値から関数subscribeToMore を取得し実装する。
  2. useSubscriptionのパラメータonSubscriptionDataに関数を実装する。

1.useQueryの返値から関数subscribeToMore を取得し実装する。

詳細はこちら。

https://www.howtographql.com/react-apollo/8-subscriptions/
関数subscribeToMoreのパラメー document にSubscriptionのgql文を記載し、パラメータ updateQueryにSubscribeされた際の処理を記述します。
updateQueryのパラメータには以下が渡されます
  • prev: このuseQueryを実行したの結果(=localCacheの状態)
  • subscrip:パラメータ document に設定したsubscription で定義し、サーバーから subscribe されたデータ

通常のJavascriptであればこの記載で問題なく対応できますが、TypeScriptの場合はこのsubscriptionDataの型がanyもしくはuseQueryを以下のように呼び出した際のinterfaceの型が適用されます。(以下の場合はFeedResultが適用)

const FEED_QUERY = gql`
  {
    feed {
      id
      description
      url
    }
  }
`;
const NEW_LINKS_SUBSCRIPTION = gql`
  subscription {
    feed{
      id
      description
      url
    }
  }
`;


const { loading, data, error, subscribeToMore } = useQuery<FeedResult>(FEED_QUERY);

subscribeToMore({
  document: NEW_LINKS_SUBSCRIPTION,
  updateQuery: (prev, { subscriptionData }) => {
    // ここでprevに含まれるLocalCache(以前のQueryの結果)にsubscribeされた
    // データをマージして返したい。
    return prev;
  },
});

ここで困るのが、Queryでは配列として複数のデータを取得する前提での処理となりますが、Subscribeされるデータは単数のデータとなります。
そのため、認識される型と実際のデータの型が異なる状態となるため、型の情報に従って実装すると実行時エラーとなり、実データの構造に従って実装するとコンパイルエラーとなります。
@ts-ignoreなどで無視するということも可能かとは思いますが、折角の型定義なので活かした記述をすべきですので、TypeScriptを用いてSubscriptionを利用したい場合は、こちらのsubscribeToMoreではなく、useSubscriptionを利用するのが適切かなと思います。

ちなみに、prevにはuseQueryで取得している結果が格納されています。
(この例では prev.feed に Link 型の配列で格納)
prevに subscriptionData で取得した結果をマージすることで想定した処理になるはずです。
(試しにやってみましたが、feed に push した箇所でobject is not extensibleのエラーとなったので最後まで検証していません。いずれにせよ subscriptionData の型を上手く合わせられなかったので途中であきらめています。 TypeScript ではなく、 JavaScript での構成であれば試す価値もあると思います)

2.useSubscriptionのパラメータonSubscriptionDataに関数を実装する。

こちらの手法についてはapolloのドキュメントページの以下を参考にしています。

https://www.apollographql.com/docs/react/development-testing/static-typing/#usesubscription
ただし、こちらの例ではSubscribeされたデータをそのまま画面に反映させるのみの例となっており、事前に取得した一覧にsubscribeされた最新のデータを追加するという例にはなっていません。
そこで、今度は useSubscription のAPIリファレンスを参照してみます。
https://www.apollographql.com/docs/react/data/subscriptions/#usesubscription-api-reference
こちらの onSubscriptionData が使えそうなので試してみました。
まずはSubscribeされた際に利用する型を以下で定義します。
interface NewLinkResult {
  feed: Link;
}

続いて、useSubscriptionの実装です。


  useSubscription<NewLinkResult>(NEW_LINKS_SUBSCRIPTION, {
    onSubscriptionData: ({ client, subscriptionData }) => {
      const feed = client.cache.readQuery<FeedResult>({
        query: FEED_QUERY,
      });

      const newLink: Link = {
        id: subscriptionData.data?.feed.id,
        url: subscriptionData.data?.feed.url,
        description: subscriptionData.data?.feed.description,
      };

      const newFeed = feed?.feed.map((item) => item);

      newFeed?.push(newLink);

      client.cache.writeQuery({
        query: FEED_QUERY,
        data: {
          feed: newFeed,
        },
      });
    },
  });

以下のように

useSubscription<NewLinkResult>(DocumentNode, SubscriptionHookOptions)

として呼び出すことにより

onSubscriptionData: ({ client, subscriptionData }) => {
  //最新化処理
}

のパラメータsubscriptionDataに対して NewLinkResult の型でSubscribeされたデータが引き渡されます。
また、パラメータ client には ApolloCache として、Queryしたデータの localCache が含まれているのでここからマージしたい Query 結果を取り出し、 Subscribe されたデータをマージする処理を記述します。(ここでは単純に末尾に追加しています)
マージしたデータを改めて cache.writeQuery で localCache に書き込むことで、事前に useQuery で取得・表示していた結果に反映させることができます。