22
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Graphcool、React、Apolloを使って、GraphQLのQueryとMutationを実装する

Last updated at Posted at 2018-03-25

はじめに

こちらの記事と内容が一部被っていますが、前回の様にGraphcool自体の説明は省き、Graphcoolを用いて、create-react-appで作ったReactアプリにおけるGraphQLのQueryとMutationの実装をなるべく詳細に説明したいと思います。

本アプリでは、次のような画面を作成して、簡単な記事情報の一覧と新規投稿の保存までが行えることを要件とします。
(取り敢えず、GraphQLの機能を実感する為に作ったものなので、あまり細かいエラーハンドリングやプロダクトに向けた最適化などは行っていません…)

完成イメージ

スクリーンショット-2018-03-26-2.38.44.png

実行環境

  • 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の型だけが残るようにします)

react-graphcool-sample/server/types.graphql
type Post @model {
  id: ID! @isUnique
  title: String!
  content: String
}

typeで型の宣言を行い、各フィールドの:の後ろに続くIDStringでそれらのフィールドが何の型かを示しています。
!はNot Nullとなることを指定します。
その他、これらの型やスキーマについて、詳しくはGraphQLの公式ページをご参照ください。

また、@が付いているものはDirectiveと呼ばれるもので、フィールドに対して特殊な設定をします。(上記例では、idフィールドに格納される値をユニークなものとする)
こちらも詳しくは公式のDirectivesの項などをご参照ください。

graphcool.yml(パーミッションの指定など)

今回はserverless functionは使わないので、 次のものだけが残るようにして、他はコメントアウトします。

react-graphcool-sample/server/graphcool.yml
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への接続用設定ファイルが生成されます。

react-graphcool-sample/server/.graphcoolrc
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の先頭に追加します。

react-graphcool-sample/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の値を代入します。

react-graphcool-sample/src/index.js
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に渡し、AppComponentをラップしたApolloApp Componentを最終的にレンダリングします。

react-graphcool-sample/src/index.js
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
react-graphcool-sample/src/components/Post.js
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を開いて試すことができます。

スクリーンショット 2018-03-26 2.29.04.png

Query APIに関して、記事IDを指定して単体の記事情報を取得するなど、もっと詳しく知りたい場合はこちらをご参照ください。

gqlはreact-apolloのhelper関数で、文字列をパースして、GraphQLの操作を行うために用います。

react-graphcool-sample/src/components/PostList.js
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であるAppApolloProviderによってラップされているので、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に保存させます。

react-graphcool-sample/src/components/PostForm.js
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を作っていきます。

react-graphcool-sample/src/components/App.js
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を用いて自動反映される仕組みについては、下記の続編をご参照ください。

続編(Subscriptions)

Graphcool、React、Apolloを使って、GraphQLのSubscriptionsを実装する

22
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?