こんにちわ。 OPENLOGI AdventCalendar 11日目です。
前回はGraphQLを使って実装してみようを書きましたが、主にサーバーサイドの話でしたので、今回はその続きとしてクライアント編を書こうと思います。
よく分からないという噂のGraphqlについて、クライアントサイドのライブラリってどうすればいいの?というのをできるだけ噛み砕いてみたいという趣旨です。
(サンプルのコードを書いていこうと思っていたら思ったより読み物になってしまった)
さて、GraphqlはFacebookがRelayというライブラリと共に出したということで、比較的Reactと利用されることが多いですが、GraphqlはReactのためだけのものではありません。
そもそもGraphql自体はGETリクエストやPOSTリクエストでサーバーサイドに問い合わせを行うので、クライアントライブラリなんてなくても使えるといえば使えます。
ということで、今回はReact/Relayに縛られずにクライアントから扱う事について書いていこうかと思います。利用しているUIライブラリに依存せずに使えるGraphqlのクライアントツール、 Apollo Clientを題材として、いわゆるクライアントサイドのGraphqlライブラリってなんなのかを説明していきます。
今回はサーバーサイドの実装はどうでもいいんですが、全体を通してGithubに上げているので、ソースを見たい場合はそちらを参考にしてみてください。
サンプルとして、以下のGraphqlスキーマを前提に進めます。
クエリノードは viewer
。アクセスしたユーザーがログインしている場合に、そのユーザーを返します。 Viewer
は自分の名前と feeds
を持っています。 feeds
は、そのユーザーのタイムラインに表示するための一覧、というイメージです。
type Query {
viewer: User
}
type User {
id: String
name: String
feeds: [Feed]
}
type Feed {
id: String
title: String
body: String
}
このスキーマ対して、(ログインしている状態で)以下のようなクエリを投げると
query {
viewer {
name
feeds {
title
}
}
}
こんなデータが帰ってくる想定です。
{
"data": {
"viewer": {
"name": "harada",
"feeds": [
{
"title": "advent calendar 1st day"
},
{
"title": "advent calendar 2nd day"
}
]
}
}
}
ajaxで取得してみる
さて、サーバーからGraphqlを利用してデータを取得する一番簡単な方法は、ajaxリクエストを投げることです。
axios.post('http://localhost:3000/graphql', {
query: 'query{viewer{name\nfeeds{title}}}',
}).then((res) => {
console.log(res.data);
});
こんな感じで投げればデータが取得できます。
つまり、サーバーサイドがRESTの時と(URLやbodyは異なりますが)、全く同じように扱えるといことです。現在サーバーサイドをREST APIとして実装している場合にも、Graphqlへの移行は少しづつ行えそうですね!
query{...
と文字列で書いている場所は、別ファイルに定義してインポートして使うのが分かりやすいですし筋ですが、要はGraphqlサーバーに文字列としてクエリを投げるだけなのです。
Apollo Clientを使ってみる
さて、ではApolloの提供しているGraphqlのクライアントライブラリ、[Apollo Client](APOLLO CLIENT)を使うとどうなるでしょう。
const apolloClient = new ApolloClient({
link: new HttpLink({
uri: 'http://localhost:3000/graphql',
}),
// 略
});
apolloClient.query({
query: gql`
query {
viewer {
id
name
feeds {
id
title
}
}
}
`
}).then((res) => {
console.log(res.data)
})
上記のajaxリクエストの時と違うのは、gqlタグが出てきたことくらいですね。gql
タグは、クエリの文字列をパースしてASTに変換処理をしています。
が、クエリを投げる箇所の実装自体で言えば、ほとんどajaxリクエストと変わりません。ではこのライブラリのメリットはどこにあるのか。
Graphqlライブラリの興味は、主にキャッシュと正規化にある
というわけで本題です。なぜGraphqlクライアントライブラリを使うのか。もちろんクエリを投げやすくするというのもありますが、主にレスポンスのキャッシュと、データの正規化がこれらのライブラリの本質だだと私は思っています。
例えばSPAで画面を切り替え戻した時、毎回データをフェッチしなおしてますか?それとも以前取得したデータを使って画面を描画してますか?
例えば更新処理を行なった時、更新後の値で画面を描画し直す際にはデータをフェッチしなおしてますか?レスポンスを利用しますか?
アプリケーションの特性によって変わるかもしれませんが、画面を切り替える度、また更新処理をするたびに何度も同じデータをフェッチし直すのは安全ですが、無駄でもあります。
一方、以前のデータをキャッシュして再利用するのであれば、そのデータが常に最新に保たれるよう管理する必要が出てきます。
複数のコンポーネントや画面で同じデータを表示する場合、どうやって全てのコンポーネント/画面について情報を更新するか。この基本的な解は、 データを取得した形のまま保持するのではなく、正規化してアプリケーションのストアに持たせておく という方針がよく使われます。
Apollo Clientでも同様、クエリで取得したしたデータはそのまま持っているのではなく、正規化して内部に保持されます。(Relayでも同様です)
例えば、ApolloClientを利用して、
query {
viewer {
id
name
feeds {
id
title
}
}
}
というクエリを投げた場合、以下のようなレスポンスが返ってきます。
{
"viewer": {
"id": "1",
"name": "harada",
"feeds": [
{
"id": "1",
"title": "advent calendar 1st day",
"__typename": "Feed"
},
{
"id": "2",
"title": "advent calendar 2nd day",
"__typename": "Feed"
}
],
"__typename": "User"
}
}
そしてApolloは内部的に以下のような形でキャッシュします。
{
"User:1": {
"id": "1",
"name": "harada",
"feeds": [
{
"type": "id",
"id": "Feed:1",
},
{
"type": "id",
"id": "Feed:2",
}
],
"__typename": "User"
},
"Feed:1": {
"id": "1",
"title": "advent calendar 1st day",
"__typename": "Feed"
},
"Feed:2": {
"id": "2",
"title": "advent calendar 2nd day",
"__typename": "Feed"
},
"ROOT_QUERY": {
"viewer": {
"type": "id",
"id": "User:1",
}
}
}
特定のデータを更新することができるように、正規化されていることがわかります。
Graphqlでは更新処理は Mutation
と呼ばれますが、 Mutation
もクエリと同じようにレスポンスの型定義します。
type Mutation {
updateViewerName(name: String): User
}
updateViewerName
というMutationを投げると、Userがレスポンスとして帰ってくるという定義です。そしてこれを実行するときは、
mutation {
updateViewerName(name: "name changed") {
id
name
}
}
のように投げますが、この id
と name
は、User型のプロパティになります。
Apollo Clientを使ってこのMutationを投げ流とUser型のオブジェクトが返ってくるため、内部のキャッシュをピンポイントで更新することができます。
{
"User:1": {
"id": "1",
"name": "harada",
// 省略
"__typename": "User"
},
}
こうすることによって、どの画面でUser型のデータを使っていても、一度のMutationで全てを更新するということが実現できます。レスポンスを自分でストアに更新しにいくなどの面倒な実装も不要です。
Reduxとかだとnormalizrなどを利用して同じことを実現することもできますが、Apolloではデータの取得、更新と、コンポーネントとの連携を含めて一つのライブラリで実現されているため、割と直感的に定義することができます。
また、Optimistic Update と呼ばれる機構も持っており、Mutationを投げた後、レスポンスが帰る前に内部データを楽観的に置き換える処理を入れることもできたりします。実データとして持っているので、画面側で複雑な分岐処理を入れなくてもすむため割と綺麗に実装できちゃったりします。
最後に
Graphqlは単に文字列を使ってフェッチする実装をすることはできます。それでもクライアントライブラリを使うと、意識せずにデータを管理することができるようになる気がしませんか?
これが、RelayとかApollo Clientといったクライアントライブラリのお仕事です。
しかしあえて触れませんでしたが、 配列へのデータの追加と削除 のような処理をする場合、このキャッシュの更新機構は少し面倒になります。正規化して持っているものの、配列への追加、削除は、 どの配列を変更するか がライブラリからは暗黙的にはわかりません。
複雑なアプリケーションになれば、同じ型の配列はいろんな形で持つことがあるため、更新処理の結果をどう反映させるか、Mutationの処理はよく考えて設計する必要がありますし、メンテナンスもわりと大変です。
というわけで、ここまで書いてあれなんですが、実は割と素のAJXリクエストをそのまま使うのも一つのてなんじゃないかなと個人的には思ってたりもするんですが、、、
同時に何かそのうちいい未来が待ってるんじゃないかなと淡い期待を抱いています。