TypeScript+react+apollo/client でSubscriptionを実現する
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つの方法があります。
- useQueryの返値から関数subscribeToMore を取得し実装する。
- useSubscriptionのパラメータonSubscriptionDataに関数を実装する。
1.useQueryの返値から関数subscribeToMore を取得し実装する。
詳細はこちら。
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のドキュメントページの以下を参考にしています。
そこで、今度は useSubscription のAPIリファレンスを参照してみます。
こちらの 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 で取得・表示していた結果に反映させることができます。
Discussion