こんにちは、クリラボユニットの早津です。
今回はGraphQLのクライアントであるApollo Clientについて取り上げてみます。 GraphQLついてはこの記事では言及しないので以下を参考にしてみてください。(とてもわかりやすくも網羅的にまとまっていて大変助かりました!)
これからReactを使用していきますが、Apollo ClientはVueやAngularなどにも対応しています。特定のフレームワークなどに依存しないのも魅力です。
では簡単なGitHubクライアントを例に説明していきます。 機能は以下になります。
機能
検索フォームでテキストを入力し該当するリポジトリのリストを表示する
シンプルの一言ですね。
バージョンの確認
ライブラリ | version |
---|---|
apollo-client | 2.6.0 |
graphql | 14.3.1 |
react | 16.8.6 |
react-dom | 16.8.6 |
react-apollo | 2.5.6 |
typescript | 3.4.5 |
環境構築
さくっと環境構築をしたいのでcreate-react-appを使用します。 typescriptを使いたいので以下のコマンドを実行します。
create-react-app アプリケーション名 --typescript
tsconfig.json
の設定を変更します。
{ "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "esnext", "noImplicitAny": false, "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "preserve" }, "include": [ "src" ] }
必要なライブラリをインストール
まずはapollo clientを使う上で必要なものをインストールします。
yarn add apollo-client apollo-link apollo-link-http graphql graphql-tag react-apollo apollo-cache-inmemory
UIを作っていく上で必要なライブラリをインストール
yarn add @emotion/core @emotion/styled @material-ui/core
GitHub Tokenの取得
GitHubのapiを使用するのでGitHubのtokenを取得します。
以下のページへ移動してください。(GitHubでサインインしていることを前提としています。)
https://github.com/settings/tokens
以下のようにrepoに全てチェックをつけてください。
チェック後、「Generate token」でtokenを発行してください。
その後、プロジェクトフォルダ直下に.env
ファイルを作成して以下のように設定してください。
REACT_APP_GITHUB_TOKEN=先ほど作成したtoken
Apollo Clientの設定
src/index.tsx
を以下のように編集してください。
import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import { InMemoryCache } from "apollo-cache-inmemory"; import ApolloClient from "apollo-client"; import { createHttpLink } from "apollo-link-http"; import { ApolloProvider } from "react-apollo"; const cache = new InMemoryCache(); const httpLink = createHttpLink({ uri: "https://api.github.com/graphql", headers: { authorization: `Bearer ${process.env.REACT_APP_GITHUB_TOKEN}` } }); const client = new ApolloClient({ link: httpLink, cache }); ReactDOM.render( <ApolloProvider client={client}> <App /> </ApolloProvider>, document.getElementById("root") );
追加したものについて簡単に説明していきます。
const cache = new InMemoryCache();
上記ではGraphQLから取得したものをキャッシュに保存しておくためにInMemoryCache
を初期化します。
const httpLink = createHttpLink({ uri: "https://api.github.com/graphq", headers: { authorization: `Bearer ${process.env.REACT_APP_GITHUB_PERSONAL_ACCESS_TOKEN}` } });
この部分はGraphQLサーバーにリクエストをするための設定です。 GitHubのapiを使用するためにheader情報に先ほど追加したtokenを設定しています。
const client = new ApolloClient({ link: httpLink, cache }); ReactDOM.render( <ApolloProvider client={client}> <App /> </ApolloProvider>, document.getElementById("root") )
最後に上で設定したものをApolloProviderのpropsに渡します。 これによってApp以下のコンポーネントからGraphQLにアクセスできるようになります。
Apollo Clientで開発していく上で必要な開発者ツールを入れましょう。 以下のリンクから追加してください。
その後、以下のようにChromeの開発者ツールの中にApolloのタグを開き、 試しにクエリを入力し実行してみます。 レスポンスの結果として右側にreactのリポジトリの総数が表示されています。
stateを定義する
Apollo Clientではフロントでもつべき状態(選択状態など)を管理する方法があります。 方法については後に説明します。
src/state.ts
を作成します。
type State = { searchText: string; __typename: string; }; const initialState:State = { searchText: "", __typename: "State" }; export default initialState;
今回は検索用のテキストしかないので記述量は少ないです。
resolverを定義する
Apollo Clientにおけるresolverはローカルのキャッシュを書き換えたり、取得する際に使用します。今回はGraqhQLサーバーから取得したデータとは別に検索窓のテキストをキャッシュに乗せるようにします。そうすることでフロントで持つべき状態とサーバーから取得したデータを一元管理することができます。
resolverについて
https://www.apollographql.com/docs/react/essentials/local-state#local-resolvers
apollo Clientのバージョンが2.5より前の時、キャッシュにフロントの状態をのせる場合はapollo-link-stateを使用していました。ですがバージョン >= 2.5からapollo-link-stateがapollo clientに統合されたのでapollo-link-stateを別途インストールする必要がなくなりました。
apollo-link-stateを使用した場合の実装の例(一部)
import { withClientState } from "apollo-link-state"; const stateLink = withClientState({ cache, defaults: initialState, resolvers: resolvers });
このバージョンの違いで実装が変わるので一度以下の記事を参考にしてください。
https://www.apollographql.com/docs/react/essentials/local-state#migrating
https://blog.apollographql.com/announcing-apollo-client-2-5-c12230cabbb7
このようにapollo clientはデータを取得したり(query)、更新したり(mutation)する以外にフロントの状態を管理してくれる機能を提供してくれるのが強いですね。
src/resolvers.ts
を作成してください。
import gql from "graphql-tag"; const resolvers = { Mutation: { changeSearchText: (_, { text }, { cache }) => { const query = gql` query SearchText { searchText @client } `; cache.writeQuery({ query, data: { searchText: text } }); } } }; export default resolvers;
changeSearchTextが検索窓のテキストを変更したときに呼ばれる関数です。
https://www.apollographql.com/docs/graphql-tools/resolvers#resolver-function-signature
その中で先ずキャッシュから変更したいものを取得します。
const query = gql` query SearchText { searchText @client } `;
その後、cache.writeQuery
でキャッシュを書き換えます。
cache.writeQuery({ query, data: { searchText: text } });
writeQueryの引数のオブジェクトにqueryと変更したいデータを書きます。 特定のidをもつデータを変更したい場合は、writeFragmentを使用します。
これでresolverの設定は終了です。
先ほど設定したresolverとstateを使えるように設定します。
src/index.tsx
を編集します。
import React from "react"; import ReactDOM from "react-dom"; import { InMemoryCache } from "apollo-cache-inmemory"; import { ApolloLink } from "apollo-link"; import ApolloClient from "apollo-client"; import { createHttpLink } from "apollo-link-http"; import { ApolloProvider } from "react-apollo"; import initialState from "./state"; import resolvers from "./resolvers"; import App from "./App"; const cache = new InMemoryCache(); const httpLink = createHttpLink({ uri: "https://api.github.com/graphql", headers: { authorization: `Bearer ${ process.env.REACT_APP_GITHUB_TOKEN }` } }); const link = ApolloLink.from([httpLink]); const client = new ApolloClient({ link, cache, resolvers }); cache.writeData({ data: initialState }); ReactDOM.render( <ApolloProvider client={client}> <App /> </ApolloProvider>, document.getElementById("root") );
追加部分を順にみていきます。
const link = ApolloLink.from([httpLink]);
上記ではhttpLink
などlinkというものを新しく追加しました。
apolo clientではこのlinkという概念がとても重要です。
linkとはGraphQLの操作(orperation)の結果をどんな風に取得したいのか、そしてその結果に対して何をしたいのかを定義することができます。
例えば以下のようなことをすることが可能です。
- エラーハンドリング
- サーバーからのレスポンスのデータ加工
- キャッシュにフロント側で持っておきたい状態を保存(apollo-link-state)
今回はapollo-link-httpしか追加していないですが、エラーハンドリング用のlinkや独自でlinkを作成することもできます。 linkについては以下を参考にしてみてください。
const client = new ApolloClient({ link, cache, resolvers });
上記ではresolverを追加しました。
cache.writeData({ data: initialState });
stateをキャッシュに書き込んでいます。
次にリポジトリの情報を表示するためのコンポーネントを作成します。
リポジトリの情報を表示するコンポーネントを作成
src/components/Repository.tsx
を作成します。
propsで受け取る情報は以下です。
- リポジトリのurl
- リポジトリの名前
- リポジトリのスター数
コードは以下のようになります。
import * as React from "react"; import { Card } from "@material-ui/core"; import Typography from "@material-ui/core/Typography"; import Link from "@material-ui/core/Link"; import styled from "@emotion/styled"; import { CardProps } from "@material-ui/core/Card"; const RepositoryWrapper = styled(({ children, ...props }: CardProps) => ( <Card {...props}>{children}</Card> ))` border: 1px solid #ccc; max-width: 800px; margin: 0 auto 20px auto; text-align: left; padding: 10px; `; type Props = { url: string; name: string; starCount: number; }; const Repository: React.FC<Props> = ({ url, name, starCount }: Props) => { return ( <RepositoryWrapper> <Typography> url: <Link href={url}>{url}</Link> </Typography> <Typography>name: {name}</Typography> <Typography>star: {starCount}</Typography> </RepositoryWrapper> ); }; export default Repository;
検索ロジックの作成
検索ロジックを作っていきます。 検索をする上でqueryを書かなくてはいけません。 今回はGitHubのGraphQLのapiを使用するので詳しい説明は省きますので気になる方は以下を参考にしてください。
GitHub GraphQL API v4 | GitHub Developer Guide
queryをコンポーネントとは別ファイルに書いていきたいのでsrc/queries.ts
を作成してください。
import gql from "graphql-tag"; export const getRepositories = gql` query($searchText: String!) { search(first: 10, query: $searchText, type: REPOSITORY) { nodes { ... on Repository { id name description url stargazers { totalCount } } } } searchText @client } `;
queryの引数に$searchText
という変数を定義します。検索窓に入力した値がここにはいってきます。(入ってくるように後でします。)
searchの引数についてです。
first
・・・上位n
件query
・・・ 検索キーワードtype
・・・検索するための種類(今回はリポジトリですが他にもIssueなどあります。)
また@client
というディレクティブをつけることでローカルのデータもGraphQLのクエリで取得することができます。
上記からわかるようにサーバー、キャッシュ両方から同時にでデータを取得することができるのです。
次にsrc/mutations.ts
を作成します。
import gql from "graphql-tag"; export const changeSearchText = gql` mutation($text: String!) { changeSearchText(text: $text) @client } `;
こちらも@client
がついてることでローカルのmutationを呼び出すようにしています。
SearchFieldコンポーネントの作成
src/components/SearchField.tsx
を作成します。
import * as React from "react"; import TextField from "@material-ui/core/TextField"; import { Mutation } from "react-apollo"; import _ from "lodash"; import styled from "@emotion/styled"; import { Button } from "@material-ui/core"; import { useState } from "react"; import { changeSearchText } from "../mutations"; const TextFieldWrapper = styled("div")` display: flex; align-items: center; justify-content: center; position: sticky; top: 0; background: #fff; padding: 20px; `; type Props = { text: string; onSearchRepository: (text: string) => void; }; const SearchField: React.FC<Props> = ({ text, onSearchRepository }: Props) => { const [inputText, setInputValue] = useState<string>(""); return ( <Mutation mutation={changeSearchText} variables={{ text: text }}> {changeSearchText => { const handleInputEnter = e => { setInputValue(e.target.value); changeSearchText({ variables: { text: e.target.value } }); if (e.keyCode === 13) { onSearchRepository(inputText); } }; const handleSearchButtonClick = () => { onSearchRepository(inputText); }; return ( <TextFieldWrapper> <TextField label={"検索"} value={inputText} onChange={handleInputEnter} onKeyDown={handleInputEnter} /> <Button variant={"contained"} color={"secondary"} disabled={_.isEmpty(inputText)} onClick={handleSearchButtonClick} > 検索 </Button> </TextFieldWrapper> ); }} </Mutation> ); }; export default SearchField;
ここで重要なのがMutation
コンポーネントです。
キャッシュの中にあるデータの更新処理を行います。
Mtutation
、Query
(後に紹介する)コンポーネントはrender Propパターンで実装されています。
Mutationのpropsに先ほど定義したmutationとvariablesを設定します。
キャッシュの中にあるsearchText
を更新しています。
src/App.tsx
を編集します。
import React, { useState } from "react"; import { Query } from "react-apollo"; import { CircularProgress } from "@material-ui/core"; import Typography from "@material-ui/core/Typography"; import styled from "@emotion/styled"; import _ from "lodash"; import { getRepositories } from "./queries"; import Repository from "./components/Repository"; import SearchField from "./components/SearchField"; const ProgressWrapper = styled("div")` && { width: 100vw; height: 100vh; position: absolute; top: 0; display: flex; justify-content: center; align-items: center; } `; const App = () => { const [text, setText] = useState<string>(""); const handleSearchRepository = text => { setText(text); }; return ( <div> <SearchField text={text} onSearchRepository={handleSearchRepository} /> <Query query={getRepositories} variables={{ searchText: text }}> {({ data, loading, error }) => { if (loading) return ( <ProgressWrapper> <CircularProgress /> </ProgressWrapper> ); if (error) return `Error! ${error}`; return ( <div> {_.isEmpty(data.search.nodes) && ( <Typography align={"center"}> 検索結果がありません。もう一度検索し直してください😭 </Typography> )} {data.search.nodes.map(repo => { return ( <Repository key={repo.id} url={repo.url} name={repo.name} starCount={repo.stargazers.totalCount} /> ); })} </div> ); }} </Query> </div> ); }; export default App;
先ほどはMutationコンポーネントを使いましたが、データを取得するためにreact-apolloが提供してくれているQuery
コンポーネントを使用します。
Queryコンポーネントはqueryを指定します。処理の内容がMutationとは異なりますが使い方としてはほとんど同じです。
Queryコンポーネントのrender propの中では以下が受け取れます。
data
・・・GraphQLから取得したデータloading
・・・ローディング状態error
・・エラー状態
ローディング状態やエラーの状態を自動的に取得もこのように手軽にできるのです。
例えばエラーの状態は以下のように表示されます。
これでGitHubクライアントの完成です。
Apolloの開発ツールを開いてキャッシュがどのように保存されているか確認してみたり、検索して試してみてください。
今回は簡単なものだったのでmutationの数は少ないですが、一つのコンポーネントの複数のmutationなどを渡したいといったときにrender propでネストした状態が生まれてしまいます。それを回避するにはどうすれば良いかなど考えてみるのも面白いかと思います。
以上になります。