クライアント側でREST APIに対してGraphQLクエリを利用可能にしてくれるライブラリ、それが apollo-link-restです。
https://github.com/apollographql/apollo-link-rest
試してみたかったのでQiita API叩いて記事毎のいいね率を算出するものを作ってみました。
まだ他に色々足していく予定なので内容は変わりますが、コードはここにあります。
使い方と感想を殴り書きします。
使い方
Linkの設定
まずはLinkの設定。ヘッダーにトークン入れるのと対象のREST APIのURLを書く。
import { RestLink } from 'apollo-link-rest';
const restLink = new RestLink({
headers: {
authorization: `Bearer ${process.env.REACT_APP_QIITA_TOKEN}`
},
uri: 'https://qiita.com/api/v2/'
});
const client = new ApolloClient({
cache,
link
});
クエリを書く
今回は自分の投稿の情報を取得するクエリを用意しました。
users/seya/items
のAPIだと投稿のPV数が取れない罠があったので、item毎にID指定でデータ取ってくるクエリも書いています。
// 本来的にはURL内の`seya`の部分はトークンを元にユーザ情報取得して、そこから代入すべきだがめんどくさいので省いた
const userItemsQuery = gql`
query userItems{
items @rest(type: "Item", path: "users/seya/items?per_page=100") {
id
title
created_at
likes_count
}
}
`;
const itemQuery = gql`
query($id: ID!) {
item(id: $id) @rest(type: "Item", path: "items/:id") {
page_views_count
}
}
`;
内容はごくシンプルで、pathの部分にはリクエストを送る対象のURLを書く。
query内のプロパティ名(userItemsQuery
でいう items
)には任意の文字列を入れる。後々取得したデータを参照する時にこのプロパティ名が用いられる。
中身のプロパティ名は取得したいデータをREST APIの戻り値と同じ名前で入れていく。
例えば "投稿(item)" のデータのフォーマットはQiita API ドキュメントでは以下のように定義されている。
参考: https://qiita.com/api/v2/docs#get-apiv2usersuser_iditems
[
{
"rendered_body": "<h1>Example</h1>",
"body": "# Example",
"coediting": false,
"comments_count": 100,
"created_at": "2000-01-01T00:00:00+00:00",
"group": {
// hogehoge
},
"id": "4bd431809afb1bb99e4f",
"likes_count": 100,
"private": false,
"reactions_count": 100,
"tags": [
//hogehoge
],
"title": "Example title",
"updated_at": "2000-01-01T00:00:00+00:00",
"url": "https://qiita.com/yaotti/items/4bd431809afb1bb99e4f",
"user": {
// hogehoge
},
"page_views_count": 100
}
]
欲しいデータは 「ID、タイトル、投稿日時、いいね数」なので対応「id, title, created_at, likes_count」をクエリ内に書いた。
React コンポーネントと繋ぎこみ
あとはreact-apolloのQueryコンポーネントで呼び出すだけ
const Prototype = () => (
<Query query={userItemsQuery}>
{({ loading, data }) => {
if (loading) {
return <p>...loading</p>;
}
const items: any[] = data.items;
return (
<table>
<thead>
<tr>
<th>タイトル</th>
<th>日付</th>
<th>いいね数</th>
<th>PV数</th>
<th>いいね率</th>
</tr>
</thead>
<tbody>
{items.map(item => (
<tr>
<td>{item.title}</td>
<td>{moment(item.created_at).format('YYYY/MM/DD')}</td>
<td>{item.likes_count}</td>
<Query query={itemQuery} variables={{ id: item.id }}>
{({ loading: itemLoading, data: itemData }) => {
if (!itemLoading && itemData) {
const pageViewsCount = itemData.item.page_views_count;
const likesPerViews = roundFloat(
(item.likes_count / pageViewsCount) * 100,
2
);
return (
<React.Fragment>
<td>{pageViewsCount}</td>
<td>{likesPerViews}%</td>
</React.Fragment>
);
}
return null;
}}
</Query>
</tr>
))}
</tbody>
</table>
);
}}
</Query>
);
汚いコードであることは見逃してほしいのですが、とりあえずこれで表示することができました。
全然違う話ですが Query コンポーネントを一コンポーネント内でネストすると loading
とか data
のネームスペースが被る問題が出てくるんだなあと思いました。
上記の例で言うと item
コンポーネントとして切り出すべきですが。
キャッシュする
本題のapollo-link-restとはそれますが、ユースケース的にサードパーティのREST APIを対象とすることがおそらく多く、うっかり上限リクエスト数越えやすいのでキャッシュもしておきます。Qiita APIは1ユーザあたり1時間1000リクエストなのでホットリロードしまくってると割と余裕で超える。
apollo-cache-persistを使います。
https://github.com/apollographql/apollo-cache-persist
import { persistCache } from 'apollo-cache-persist';
persistCache({
cache,
storage: window.localStorage
});
これでローカルストレージに勝手にクエリの結果をキャッシュしてくれる。
デフォルトでは全てのクエリの結果を自動でキャッシュする。
クエリを変えずに値を更新したい場合は明示的にrefecthしたりキャッシュを解放したりしないといけない。
感想
使いやすさ
クエリの書き方は直感的で使いやすい。
コンポーネントとの繋ぎこみも非常に簡単なので楽しい。(これはどちらかと言うとreact-apolloのおかげですが)
プロダクションに使う価値があるか
「GraphQL APIがある、且つサードパーティのRESTのAPIを使う必要があり、データを一元的に管理したい場合」みたいな状況ならアリ、というか多分データの管理がしやすいので使った方がいいと思う。
「GraphQL APIがない場合に、 Apollo Clientを状態管理ツールとして使う目的でapollo-link-rest + apollo-link-state で行きたいかどうか」という問いに関しては「できることなら普通にサーバサイドでGraphQL on RESTやった方がいいと思う」というのが個人的な意見です。
第一に「余計なデータ転送がサーバ → クライアント間のネットワークで増える」という問題があります。
また、apollo-link-restで扱うデータは GraphQLのスキーマみたいに自分で定義するわけではなく、既存のREST APIで定義されたフォーマットに一定従わざるを得ないので、比較すると柔軟性は微妙に下がるなと感じました。
裏を返せばスキーマとresolver書く必要がなくなるので楽と言えば楽なんですが、「GraphQLとは…」みたいな気持ちになることは否めない。
また、フロントエンドしか触れない人がプロトタイプに既存のREST API使って何か作る、みたいなユースケースもありだと思いました。(というかREADMEに例として挙がっている)