こんにちは。いかがコーディングお過ごしでしょうか。
私は今更ながら最近GraphQLで遊び出し、そしてApollo Clientに出会いました。
ワクワクしました。「これは想像以上に既存のフロントエンドの設計・実装を変えるものだぞ!」と感じました。
「Apollo ClientってGraphQLクライアントでしょ?GraphQLエンドポイントない俺には関係ないな。」と思ったそこのあなた、それだけじゃないんですApollo Clientは!!!!!
本記事では「Apollo Clientとはなんぞや」という話と「なぜ私がApollo Clientを布教したいのか」という点について語ります。実は最初は実装含めたチュートリアルを書いていたのですが長くなり過ぎたため記事を二つに分けました。この記事はどちらかと言うと概念系の話が多めで、片方にApollo Client + Reactのチュートリアルを書いたのでぜひ合わせてお読みください。
Apollo Client + React 入門
今回紹介するApollo Clientはver.2が半年ほど前に出たばかりでベストプラクティスもまだまだこなれておらず、特に日本ではそもそも記事が全く見当たりません。本記事をきっかけに興味を持つ人が増えてくれると嬉しいです。
GraphQLとは
Apollo Clientは何かを知る前にはGraphQLとは何なのかを知る必要があるのですが、さすがにこれについては既にいい記事がゴロゴロしているのでリンクを貼っておくに留めます。
以上です。…と言いたいところですが、ここで一点だけGraphQLの重要な特徴を述べます。
それは「GraphQLは"ユニバーサルな"クエリ言語である」ということです。まずGraphQL自体はフレームワークや特定の技術を指すのではなく、データに対するクエリ言語の仕様を表しています。そしてこのクエリを送る対象は何もGraphQLクライアントだけではありません。実装によってはブラウザ内のキャッシュやREST APIにすらGraphQLのクエリを扱うことができます。
これに関しては後ほどApollo Clientのエコシステムと合わせてご紹介させていただければと思うので、一旦「ユニバーサル」というキーワードだけ頭に刻みつけてください。
Apollo Clientとは
Apollo ClientとはGraphQL APIをシンプルにクライアント側で操作するためのライブラリです。
詳しい実装は冒頭に載せた別記事に書いているのですが、簡単にコード例をこちらでも紹介いたします。
まずはLink
というものを用いて利用するGraphQLエンドポイントを設定します。
import { ApolloProvider } from 'react-apollo';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import App from './App';
const GITHUB_BASE_URL = 'https://api.github.com/graphql';
const httpLink = new HttpLink({
uri: GITHUB_BASE_URL,
headers: {
authorization: `Bearer ${
process.env.REACT_APP_GITHUB_PERSONAL_ACCESS_TOKEN
}`
}
});
const cache = new InMemoryCache();
const client = new ApolloClient({
link: httpLink,
cache
});
そして Appollo Provider
でアプリ全体のコンポーネントを括ってあげれば準備OKです。(react-reduxのProviderと全く同じ働きと思っていただいて問題ないです) これで下位のコンポーネントから先ほど設定したクライアントに対して参照を持てるようになります。
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
そしてGraphQLエンドポイントのデータとコンポーネントの繋ぎこみは以下のように行います。
const GET_CURRENT_USER = gql`
{
viewer {
login
name
}
}
`;
const User = () => (
<Query query={GET_CURRENT_USER}>
{/* Render Prop パターンでクエリの結果にアクセスしています */}
{({ data, loading, error }) => {
if (error) {
return <ErrorMessage error={error} />;
}
const { viewer } = data;
if (loading || !viewer) {
return <Loading />;
}
return (
<div>{viewer.name}</div>
);
}}
</Query>
);
このようにGraphQLエンドポイントに対する問い合わせをシンプルにしてくれ、またReactやVueなどのUIライブラリとの繋ぎこみもサポートしてくれるのがApollo Clientです。そして、先の項で述べた通り、GraphQLは「ユニバーサル」なクエリ言語であり、GraphQL API以外のデータソース、ローカルのデータやREST APIに対しても使えます。
apollo-link-state
先ほど述べた「ユニバーサルなクエリ言語」を実現するのに重要なのがこのapollo-link-state
というライブラリです。apollo-link-state
とはその名の通りローカルのデータに対してもGraphQLクエリで扱えるようなStoreを提供してくれるApollo Clientのライブラリです。
簡単な実装イメージを紹介しますと、まずは以下のようにLink
を設定します。またこの時に初期の状態も指定します。
import { withClientState } from 'apollo-link-state';
const initialState = {
selectedItemId: 'hogehoge',
};
const stateLink = withClientState({
cache,
defaults: initialState,
resolvers: {},
});
const link = ApolloLink.from([stateLink, httpLink]);
const client = new ApolloClient({
link,
cache,
});
そしてクエリを発行する際は「このデータはローカルに存在しますよ」ということを示す @client
というディレクティブを指定します。これによってローカルのデータに対してもGraphQLクエリを使用することができます。
const GET_SELECTED_ITEM = gql`
query {
selectedItemId @client
}
`;
ちなみに一つのクエリ内にGraphQLエンドポイントからのデータとローカルのデータを指定することも可能です。
const GET_SELECTED_REPOSITORIES = gql`
query {
selectedItemId @client
item {
name
description
}
}
`;
apollo-link-rest
apollo-link-rest はその名の通りREST APIに対してGraphQLクエリを利用可能にしてくれるものです。なので、理論上はGraphQLサーバーがなくともクライアント側でGraphQLを使うことは可能なのです!!!!
まあそれをしたいかというのと、apollo-link-rest
のREADMEに「⚠️ This library is under active development ⚠️」と書いてある現状、プロダクションに使える選択肢かは微妙ですが。
apollo-link-rest
の使い方は apollo-link-state
と同様で、専用のLink
を足してあげて、クエリを発行する際に @rest
のディレクティブを指定するのみです。
なぜApollo Clientが俺達の魂を震えさせるのか
それでは、既に魂が震えてくれた方もいらっしゃるかもしれないですが、本題のなぜ私がApollo Clientを布教したいのかという点について語ります。
結論から述べると「状態管理におけるクライアント側のコードがシンプルになるから」が理由です。
「GraphQLはサーバサイドのクエリ言語でしょ?状態管理となにが関係あるの?」そう思われた方も多いのではないでしょうか。
順を追って説明いたします。
まず最初に、状態管理とはなんぞやという話ですが、個人的にフロントエンドの状態管理は以下のような世界観だと考えています。(オレオレモデルなので正直マサカリが怖いのですがツッコミどころあればコメントください。心が保てば反応します。)
設計の際に重要になる観点は次の3つです。
1. データフローマネージメント
昨今のフロントエンドアプリケーションではバグの少なく状態遷移の予測可能性が高まるよう単方向のデータフローに制御することが主流となっており、このフロー制御にfluxなどのアーキテクチャやRxが用いられています。
2. データソースとの同期、それに伴う非同期処理
フロントエンドのコードはそれ単体で完結することは少なく、大抵外部からデータを取得したり、ユーザのアクションに応じたデータの同期処理を行う必要があります。ReduxがSingle source of truth と言ってもそれはローカルのデータに限った話であり、source
は他にもあるのです。また、要求によっては非同期に起こる状態変化は複雑になり、これをハンドリングするためにRxやReduxのmiddlewareなどのツールを利用します。
3. 状態からUIを表現する
状態は最終的にUIに反映されます。Virtual DOMのおかげで大抵の場合は状態とDOMのマッピングを書くだけで済むことが多いですが、時にそれだけでは対応できない複雑な状態遷移が起こることがあり、それに対応するためにコンポーネントの設計パターンだったりState Machineの考え方が有用になります。先に述べておくとここに関してはApollo Clientそんなに関係ないのと、あと正直そんなに考えまとまってないので話題として触れといてなんですが、本記事ではこれ以上は語りません。
それでは、上図に Apollo Clientを導入するとこうなります。
何が変わったかを簡潔に述べると「今まで書いていた状態変化に関わるロジックがGraphQLクエリで宣言的に書けるようになった」ということです。
具体的に説明します。私が思うにActionが実行されてからViewがアップデートされるまでの部分で書かれるロジックには大きく以下の4種類があります。
- データソースへの問い合わせ・同期
- ローカルのデータをデータソースに対するリクエストのパラメータに合う形に加工する
- 取得したデータをViewが使いやすい形に加工する
- Actionにともないローカルデータのアップデート
この内1〜3がクエリで宣言的に表現できるようになるとお伝えすればシンプルさが伝わるのではないでしょうか。
まず1点目の「1. データソースへの問い合わせ・同期」に対して、これは大元のApollo Client
オブジェクトにて定義すればあとは心配する必要はありません。
GraphQLクライアントを扱う際の重要な登場人物として「スキーマ」と「resolver」がいます。スキーマはその名の通りで、クエリで取ってくるオブジェクトのスキーマを定義します。そしてresolverはそのスキーマと実際のデータソースをどうマッピングするかを定義します。
このスキーマ・resolverの定義とApollo ClientのLinkの設定によりクライアント側のコードは「どこにどんなデータがあるか」を知る必要がなくなり、単純にクエリを発行すれば良いだけ、となります。(厳密に言うと @client
とか @rest
とかつける必要はあるんですが)
これにより我々は(クライアント側のコード視点で)真の意味での Single source of truthを手に入れることができるのです。
2点目、3点目に関してはそもそものGraphQLが生まれた目的なため言わずもがなでしょう。単純にViewが扱いやすいようなクエリを書けばいいだけですね。すごいあっさり書いていますが、これによるコード量の減少は計り知れません。ReduxやVuexなどでコードを書いている方はデータ成形の処理が全部消えることを想像してみるといかに大きい変化かが想像できると思います。
4つ目も楽勝!と申し上げたいところなのですが、こちらに関しては残念ながら従来のやり方と劇的に変わるということはありません。残念なところなので省こうかとも思ったのですが、良心に従い解説します。若干蛇足になるので「残念ならいいや…」という方は適宜読み飛ばしてください。
そもそも「Actionにともないローカルデータのアップデート」が何を指しているのかという話なのですが、例を上げるとQiitaのいいね!ボタンです。
Qiitaのいいね!ボタンは押したときにサーバーに対して更新のためのリクエストが送られます。
そのときクライアント側でもちゃんと数値が増えますね(試しにこの記事内のボタンを押してみましょう。…ほらっ!)
これを実現する方法はいくつかあります
- アップデートのリクエスト送った際に最新の数を返すようなAPIに設計する
- アップデートのリクエストを送った後にいいね数を再フェッチする
- いいねを押した時にクライアント側で
1
増やす処理を行う
3番目の選択肢を取ることにが多いのではないかと思うので、この方針で実装例を書いてみます。
これをApollo Clientで実現する際は以下のような感じになります。
const updateLikes = (
client,
{
data: {
updateLikes: {
article: { id }
}
}
}
) => {
const article = client.readFragment({
id: `Article:${id}`,
fragment: ARTICLE_FRAGMENT
});
const likeCount = article.likeCount + 1;
client.writeFragment({
id: `Article:${id}`,
fragment: ARTICLE_FRAGMENT,
data: {
article: {
totalCount
}
}
});
};
<Mutation
mutation={LIKE_ARTICLE}
variables={{ id }}
update={updateLikes}
>
{(likeArticle, { data, loading, error }) => (
// 何かしらの処理
)}
</Mutation>
そんなにめんどくさい訳でもないですが、楽になった訳でもないですね。
あるデータとデータの関係性を宣言的に書いたらOKみたいなのって実現できないものか。
まとめ
話が広がってしまいましたが、まとめると私が布教したい理由は「状態管理に関するロジックがシンプルになる」に尽きます。まだまだ新しい技術ではありますが考え方は妥当性があるように思えるし、コミュニティも熱量高そうなので非常に期待しています。
コラム: GraphQLはReduxを置き換えるのか
※8/27編集
たまに海外で「GraphQLはReduxを置き換える!」みたいなタイトルをちょくちょく見かけますが、どちらかと言うと「Apollo ClientがReduxを置き換える」と捉えるべきでしょう。
少なくとも通信周りの状態に関してはApollo Clientが丸ごと役割を持っていきます。残ったローカルの状態に関してはReduxが価値を発揮できる部分はあるかもしれません。
ただ個人的にはそれだけなら大体のケースにおいて素のReactの状態管理だけで十分にハンドリングできそうだなと思っていて、そういった場合にはReduxはオーバーエンジニアリングになるなと感じています。また、何回か文中でも紹介しましたが apollo-link-state
によってローカルステートを管理してしまうこともできるので、まだこちらが有望なソリューションかは自信を持てていないですが、全てをApollo Clientで管理することも可能だと思います。
これから学びたい人のための資料
いろいろ褒めてはきましたが、やはり初期の学習コストはそこそこ高いなと感じています。GraphQL自体も世に出て3年とはいえまだまだ新しいパラダイムですし、クエリの仕様などは慣れるのに時間多少かかるなぁという印象です。
そして残念なのが日本語の資料が絶望的にないところ。
なので英語にはなってしまいますが、勉強に非常に役立ったソースをいくつか載せておきます。
うむ、なんか公式っぽいものばかりの紹介になったのが悲しいところですが頑張っていきましょう!
あとがき
Apollo Client いかがでしたでしょうか?
個人的には「全部の状態Apolloに託すと逆に煩雑になったりするんじゃないか」とか「ローカルのデータめっちゃ増えた時にドキュメントの運用とか回んのかな」とか疑問を持ちつつも、間違いなくフロントエンド状態管理の次の潮流の一つになると考えています。
GraphQLサーバーがある環境だとフロント側のGraphQLクライアントとしてはApollo Client一強になりつつあるので、それであれば状態管理の機構もApollo Clientに託してしまうのはアリだと思います。
この記事を読んで一緒に人柱になってくれる方が増えるととても嬉しいです^^
それでは、よきコーディングを!