はじめに
こちらの記事と内容が一部被っていますが、前回の様にGraphcool自体の説明は省き、Graphcoolを用いて、create-react-appで作ったReactアプリにおけるGraphQLのQueryとMutationの実装をなるべく詳細に説明したいと思います。
本アプリでは、次のような画面を作成して、簡単な記事情報の一覧と新規投稿の保存までが行えることを要件とします。
(取り敢えず、GraphQLの機能を実感する為に作ったものなので、あまり細かいエラーハンドリングやプロダクトに向けた最適化などは行っていません…)
完成イメージ
実行環境
- Mac OS Sierra: v10.12.6
- node: v8.2.0
- npm: v5.3.0
事前準備
create-react-appのインストール
npm install -g create-react-app
graphcool(GraphcoolのCLI)のインストール
npm install -g graphcool
Graphcoolへのログイン(ローカル環境の認証)
Graphcoolアカウントが作成されていなければ、公式サイトからSign Upしてアカウントを作成して、次のコマンドを実行します。
graphcool login
loginに成功すると、ホームディレクトリの直下に.graphcoolrc
が自動生成されます。
Reactアプリの作成〜起動確認
# ホームディレクトリに移動
cd ~/
# `react-graphcool-sample`ディレクトリを作成し、Reactアプリの土台を作る
create-react-app react-graphcool-sample
# react-graphcool-sampleのルートに移動
cd react-graphcool-sample/
# Reactアプリの起動を確認
yarn start
Graphcool環境の構築
Reactアプリの起動まで確認ができたら、別のターミナルを立ち上げて、 react-graphcool-sample
のルートに移動し、graphcool init
コマンドを用いて、 serverという名前のディレクトリの内部に、Graphcoolを利用するためのファイルを作成します。
(server
ではなく、任意の名前でも構いません)
cd ~/react-graphcool-sample/
graphcool init server
主に、次の3点が作成されます。
- graphcool.yml ( Graphcoolを利用するための設定ファイル。作成した型定義ファイルやserverless functionの参照、パーミッションの指定など)
- types.graphql (GraphQLのスキーマ定義ファイル)
- src/(serverless functionが格納されたディレクトリ)
types.graphql(スキーマ定義)
types.graphql を編集して、次のPost
という型の定義を追加します。(元々あったUser
の型などはコメントアウトして、Postの型だけが残るようにします)
type Post @model {
id: ID! @isUnique
title: String!
content: String
}
type
で型の宣言を行い、各フィールドの:
の後ろに続くID
やString
でそれらのフィールドが何の型かを示しています。
!
はNot Nullとなることを指定します。
その他、これらの型やスキーマについて、詳しくはGraphQLの公式ページをご参照ください。
また、@
が付いているものはDirectiveと呼ばれるもので、フィールドに対して特殊な設定をします。(上記例では、id
フィールドに格納される値をユニークなものとする)
こちらも詳しくは公式のDirectivesの項などをご参照ください。
graphcool.yml(パーミッションの指定など)
今回はserverless functionは使わないので、 次のものだけが残るようにして、他はコメントアウトします。
types: ./types.graphql
permissions:
- operation: "*"
/src
react-graphcool-sample/server/src以下は、src
ディレクトリごと削除します。
graphcool deploy
graphcool init
によって作成されたディレクトリに移動し、graphcool deploy
コマンドを実行して、上記の設定をGraphcoolのサービスにデプロイします。
このコマンドは、スキーマなどに変更があった都度、同じコマンドを実行して変更を適用する必要があります。
# serverディレクトリに移動
cd server
graphcool deploy
graphcool deploy
コマンドを実行すると、次の3つの質問をされます。
はじめの質問で、Shared Clusters(フリーで使えるクラウドのホスティングサービス)の指定を求められますので、いずれかのリージョンを指定します。
特にこだわりなければ、デフォルトの選択肢のままenterキーを押して進めましょう。
? Please choose the cluster you want to deploy to (Use arrow keys)
Shared Clusters:
❯ shared-eu-west-1
shared-ap-northeast-1
shared-us-west-2
次に、target name
を聞かれますが、こちらも特にこだわりない場合は、enterキーを押せば、デフォルトのprod
が適用されます。
? Please choose the target name (prod)
続いて、service name
を聞かれますが、こちらはデフォルトではディレクトリ名が適用されます。
? Please choose the service name (server)
デプロイに成功すると、クラウドのコンソールから該当のプロジェクトを確認することができます。
確認用URLは、https://console.graph.cool/
に続いて、graphcool init
によって作成したディレクトリ名を指定します。
上記の例では、https://console.graph.cool/server/
となります。
(コンソールにアクセスするには予め、Graphcoolにログインしている必要があります)
こちらのコンソールからデータの追加、エンドポイントの確認、プロジェクトの削除などができます。
また、デプロイに成功すると、graphcool init
によって作成したディレクトリ内に、下記のような、Graphcoolへの接続用設定ファイルが生成されます。
targets:
prod: shared-eu-west-1/xxxxxxxxxxxxxxxxxxxxxxxx
default: prod
xxxxxxxxxxxxxxxxxxxxxxxx
の部分はProject IDを示しており、次回以降のgraphcool deploy
実行時は、こちらの情報をもとに、Graphcoolのサービスに接続しにいきます。(Project IDもコンソールから確認が可能です)
エンドポイントの取得
デプロイ実行完了時のメッセージの最後に表示される、Simple API
の値を、後に続くReactアプリからのAPI接続設定に使いますので、手元に控えておきましょう。
(忘れても、server
ディレクトリ内でgraphcool info
をたたけば、次の様にエンドポイントを確認することができます)
$ graphcool info
Service Name Cluster / Service ID
────────────── ────────────────────────────────────────────
server shared-eu-west-1/xxx
API: Endpoint:
────────────── ────────────────────────────────────────────────────────────
Simple API: https://api.graph.cool/simple/v1/xxxxx
Relay API: https://api.graph.cool/relay/v1/xxxxx
Subscriptions API: wss://subscriptions.graph.cool/v1/xxxxx
Apolloの組み込み〜接続設定
GraphQL APIへ接続するためのクライアントライブラリであるApolloをインストールし、Reactアプリに組み込んで、APIへの接続設定を行います。
Apolloインストール
次の4つのパッケージのインストールを行いましょう。
yarn add apollo-client-preset react-apollo graphql-tag graphql
パッケージのインストール後、Reactアプリで使用できるように、次の記述をsrc/index.js
の先頭に追加します。
import { ApolloProvider } from "react-apollo";
import { ApolloClient } from "apollo-client";
import { HttpLink, InMemoryCache } from "apollo-client-preset";
API接続設定
続いて次のコードによって、API接続情報を持ったApolloClientインスタンスを生成します。GRAPHQL_ENDPOINT
には、先のGraphcoolデプロイ完了時に控えておいたSimple API
の値を代入します。
const GRAPHQL_ENDPOINT = ""; // Simple APIの値を代入
if (!GRAPHQL_ENDPOINT) {
throw Error("GRAPHQL_ENDPOINTが設定されていません。");
}
const client = new ApolloClient({
link: new HttpLink({
uri: GRAPHQL_ENDPOINT
}),
cache: new InMemoryCache()
});
上記インスタンスを<ApolloProvider />
のPropsに渡し、App
ComponentをラップしたApolloApp
Componentを最終的にレンダリングします。
const ApolloApp = (
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
ReactDOM.render(ApolloApp, document.getElementById("root"));
registerServiceWorker();
React Componentの作成
記事情報を一覧し、投稿するためのUIとして、Componentを作成していきます。
srcディレクトリ直下にcomponentsディレクトリを作成し、その中にComponentを格納します。
(App Componentもcomponentsディレクトリに移します)
├ public
├ server
└ src
└ components
├ App.js・・・PostListおよびPostFormをレンダリングする
├ Post.js・・・単体の記事情報(タイトルおよび内容)をレンダリングする
├ PostList.js・・・Queryによって全記事を取得し、<Post />
で各記事情報をレンダリングする
└ PostForm.js・・・Mutationによって新規記事情報を追加保存する
Post(記事情報表示)
Propsの型チェックを行うために、prop-typesのライブラリをインストールします。
yarn add -D prop-types
import React, { Component } from "react";
import PropTypes from "prop-types";
class Post extends Component {
render() {
return (
<dl>
<dt>{this.props.post.title}</dt>
<dd>{this.props.post.content}</dd>
</dl>
);
}
}
Post.propTypes = {
post: PropTypes.shape({
id: PropTypes.string,
title: PropTypes.string,
content: PropTypes.string
})
};
export default Post;
PostList(記事情報取得&一覧)
GraphQLから全記事データを取得するために、次のquery文を書きます。
query {
allPosts {
id
title
content
}
}
こちらのquery文の実行結果については、ブラウザでコンソールにアクセスまたはgraphcool init
によって作成したディレクトリ内でgraphcool playground
コマンドを実行することによって、Playground
を開いて試すことができます。
Query APIに関して、記事IDを指定して単体の記事情報を取得するなど、もっと詳しく知りたい場合はこちらをご参照ください。
gql
はreact-apolloのhelper関数で、文字列をパースして、GraphQLの操作を行うために用います。
import React, { Component, Fragment } from "react";
import Post from "./Post";
import gql from "graphql-tag";
import { graphql } from "react-apollo";
const ALL_POSTS_QUERY = gql`
query {
allPosts {
id
title
content
}
}
`;
class PostList extends Component {
render() {
if (this.props.allPostsQuery && this.props.allPostsQuery.loading) {
return <p>データを読み込み中</p>;
}
if (this.props.allPostsQuery && this.props.allPostsQuery.error) {
return <p>エラーが発生しました。</p>;
}
const allPosts = this.props.allPostsQuery.allPosts;
if (allPosts.length === 0) {
return <p>投稿がありません。</p>;
}
return (
<Fragment>
{allPosts.map(post => <Post key={post.id} post={post} />)}
</Fragment>
);
}
}
export default graphql(ALL_POSTS_QUERY, { name: "allPostsQuery" })(PostList);
graphql関数(react-apolloのヘルパー関数)は、パースしたquery(ALL_POSTS_QUERY)を第一引数に渡し、第二引数にGraphQL APIから取得したデータが格納されるProps名(allPostsQuery
)を指定したオブジェクトを渡し、Componentを引数に取って、拡張されたComponentを返す関数(HoC)を返します。
つまり、graphql(ALL_POSTS_QUERY, { name: "allPostsQuery" })
がHoCとなり、PostListを引数に取って、拡張したPostListを最終的にexportしています。
また、ルートComponentであるApp
がApolloProvider
によってラップされているので、Appの子ComponentであるPostListにProps(props.allPostsQuery
)を通じてGraphQL APIから取得したデータが伝わります。
props.allPostsQuery以下にJSON形式で取得結果のデータが入っています。
データ取得開始から完了までの間はloading
の値がtrue
に、データの取得に失敗した場合はerror
の値にErrorオブジェクトが格納されているので、render()の最初の方の処理に、これらのケースのハンドリングを行なっています。
PostForm.js(記事追加投稿)
PostListではquery
を用いてデータを取得する処理を書きましたが、今度はデータを書き込む処理を書くために、mutation
を用います。
入力フォームからのデータを変数として、GraphQL APIに渡す必要がありますが、次のように、$
をつけて変数宣言することでmutationの引数に変数を渡せます。
さらに、mutationの引数に渡った変数はcreatePost
の引数に渡され、createPost
の実行(データの書き込み)が完了すると、idを返します。
mutation createPostMutation($title: String!, $content: String) {
createPost(title: $title, content: $content) {
id
}
}
mutationに変数に値を格納するため、記事投稿用の入力フォームを作成します。
2つの入力欄を設けて、1つはタイトル、もう片方は内容を入力するためのUIとし、それぞれ<input>
、<textarea>
を<form>
内に配置して、レンダリングします。
それらのフォームの入力値を取得できるよう、入力される度にonChange
にてsetState()
を呼び出し、入力値をeventオブジェクトから取り出し、stateに保存させます。
import React, { Component } from "react";
import gql from "graphql-tag";
import { graphql } from "react-apollo";
class PostForm extends Component {
constructor(props) {
super(props);
this.state = {
content: "",
title: "",
isSending: false
};
}
createPost = async e => {
e.preventDefault();
this.setState({ isSending: true });
const { title, content } = this.state;
try {
await this.props.createPostMutation({
variables: {
title,
content
}
});
this.setState({
content: "",
title: "",
isSending: false
});
} catch (err) {
console.log(err);
}
};
render() {
return (
<form>
<div>
<input
id="title"
type="text"
value={this.state.title}
placeholder="タイトルを入力"
onChange={e => this.setState({ title: e.target.value })}
style={{ width: "500px" }}
/>
</div>
<div>
<textarea
id="content"
value={this.state.content}
placeholder="内容を入力"
onChange={e => this.setState({ content: e.target.value })}
style={{ width: "500px", height: "100px", marginTop: "10px" }}
/>
</div>
{this.state.isSending ? (
"送信中"
) : (
<button onClick={e => this.createPost(e)}>投稿する</button>
)}
</form>
);
}
}
const CREATE_POST_MUTATION = gql`
mutation createPostMutation($title: String!, $content: String) {
createPost(title: $title, content: $content) {
id
}
}
`;
export default graphql(CREATE_POST_MUTATION, {
name: "createPostMutation"
})(PostForm);
入力が確定したら、投稿する
ボタンをクリックするとcreatePost()
が非同期で実行され、送信処理が開始されます。
また、PostListのprops.allPostsQuery
同様にprops.createPostMutation
によって、Props経由で、GraphQL APIにデータを渡しています。
App.js
最後に、上記までに作成したPostList、PostFormをレンダリングするために、ルートコンポーネントのAppを作っていきます。
import React, { Component, Fragment } from "react";
import PostList from "./PostList";
import PostForm from "./PostForm";
class App extends Component {
render() {
return (
<Fragment>
<section>
<h2>投稿一覧</h2>
<PostList />
</section>
<section>
<h2>新規投稿</h2>
<PostForm />
</section>
</Fragment>
);
}
}
export default App;
完成品
ここまでのソースは下記のGitHubにあげてあります。
フォームに入力後、投稿する
ボタンをクリックすると入力内容がGraphcoolのサービスに保存されますが、保存直後において、上記の実装ではページに自動反映はされないので、保存された記事を確認したい場合は、ブラウザの更新ボタンを押してページを更新する必要があります。
更新ボタンを押さずに、GraphQLのSubscriptionsを用いて自動反映される仕組みについては、下記の続編をご参照ください。