AWS
React
GraphQL
AppSync

AWS AppSyncとReactでToDoアプリを作ってみよう(3) Reactアプリの作成

はじめに

前回の記事まででは、AWS AppSyncを使ってGraphQL APIを作成しました。
今回は、そのGraphQL APIと連携するクライアント側Reactで作成してきたいと思います。
AWS AppSyncでは、React向けのGraphQLクライアントのApolloに対応した、aws-appsync-react (バインディングライブラリ)が用意されているので、今回はこれを使って、AWS AppSyncのデータとコンポーネントの紐付けを行っていきます。

プロジェクトのセットアップ

雛形の作成

Create React Appを使って、雛形を作成します。
(今回使用したnodeのバージョンは9.2.0です。)

$ mkdir aws-appsync-todo-app
$ npx create-react-app .

追加で必要なパッケージをインストールします。

$ yarn add graphql-tag react-apollo aws-appsync aws-appsync-react uuid
  • graphql-tag
    • GraphQLのスキーマをJavaScriptのコード内に定義するために使用
  • react-apollo
    • GraphQLクライアント
  • aws-*のパッケージ
    • AWS AppSyncとApolloを連携するために使用
  • uuid
    • Todo個々のアイテムにクライアント側でユニークなIDをつけるために使用

設定ファイルの取得とインポート

コンソール画面から、「AWS AppSync > 作成したプロジェクト > Top画面」を開き、「Getting Started」の一番下にある、「Download the AWS AppSync.js config file」からAppSync.jsをダウンロードします。

スクリーンショット 2018-01-25 22.17.20.png

ダウンロードしたファイルを、作成したプロジェクトのsrc以下に配置し、他のパッケージと合わせてApp.jsにインポート

App.js
import { ApolloProvider } from 'react-apollo';
import AWSAppSyncClient from "aws-appsync";
import { Rehydrated } from "aws-appsync-react";
import appSyncConfig from "./AppSync";

AppSyncClientを初期化

設定ファイルから読み込んだ、AWS認証情報・API情報を引数に、AWS AppSyncClientを初期化します。
authパラメータで認証方式を選択できますが、今回はAPI Keyによる認証を使用します。

App.js
// AWS AppSync Client
const client = new AWSAppSyncClient({
  url: appSyncConfig.graphqlEndpoint,
  region: appSyncConfig.region,
  auth: {
    type: appSyncConfig.authenticationType,
    apiKey: appSyncConfig.apiKey,
  }
});

AppSyncのデータと連携するコンポーネントをApolloProviderRehydratedに含めApolloProviderに対して、AppSyncClientを渡しています。

App.js
class App extends Component {
  render() {
    return (
      <ApolloProvider client={client}>
        <Rehydrated>
          <div className="App">
            <header className="App-header">
              <h1 className="App-title">AWS AppSync Todo</h1>
            </header>
            <TodoListWithData />
          </div>
        </Rehydrated>
      </ApolloProvider>
    );
  }
}

GraphQLクエリの作成

graphql-tagのgqlメソッドを使って、クエリを定義します。今回は、src/GraphQL以下に作成しました。

Todo全件取得のQuery

QueryGetTodos.js
import gql from "graphql-tag";

export default gql(`
query {
  getTodos {
    id
    title
    description
    completed
  }
}`);

Todo作成のMutation

MutationAddTodo.js
import gql from "graphql-tag";

export default gql(`
mutation addTodo($id: ID!, $title: String, $description: String, $completed: Boolean) {
  addTodo(
    id: $id
    title: $title
    description: $description
    completed: $completed
  ) {
    id
    title
    description
    completed
  }
}`);

Todo更新のMutation

MutationUpdateTodo.js
import gql from "graphql-tag";

export default gql(`
mutation updateTodo($id: ID!, $title: String, $description: String, $completed: Boolean) {
  updateTodo(
    id: $id
    title: $title
    description: $description
    completed: $completed
  ) {
    id
    title
    description
    completed
  }
}`);

Todo削除のMutation

MutationDeleteTodo.js
import gql from "graphql-tag";

export default gql(`
mutation deleteTodo($id: ID!) {
  deleteTodo(id: $id) {
    id
    title
    description
    completed
  }
}`);

ToDoList Componentの実装

今回は、ざっくり1つのComponentにTodoリストの全機能をまとめてしまいます。
src/Components以下に、TodoList.jsを作成しました。

AWS AppSyncとComponentの連携

AWS AppSyncのGraphQL APIからとReact Componentを連携するために、react-apolloを使用します。

react-apolloのgraphqlメソッドの引数にComponentを渡すと、ComponentのpropsでGraphQL APIから取得したデータを取得できるComponentを受け取ることができます。

graphql
const TodoListWithData = graphql(QueryGetTodos)(TodoList);

また、複数のQuery、MutationとComponentを連携する場合には、react-apolloのcomposeメソッドを使用します。
実際には、それぞれにオプションなどを指定するため、あくまでイメージです。

compose
const TodoListWithData = compose(
  graphql(QueryGetTodos),
  graphql(MutationAddTodo),
  graphql(MutationUpdateTodo),
  graphql(MutationDeleteTodo),
)(TodoList);

実際に、それぞれのQuery、Mutationを紐付ける部分は次の通りです。

TodoList.js
export default compose(
  // 全件取得Query
  graphql(QueryGetTodos, {  // あらかじめ定義したGraphQLクエリを使用
    options: {
      fetchPolicy: 'cache-and-network'
    },
    props: (props) => ({
      todos: props.data.getTodos
    })
  }),
  // 追加Mutation
  graphql(AddTodoMutation, {  // あらかじめ定義したGraphQLクエリを使用
    props: (props) => ({
      onAdd: (todo) => {
        props.mutate({
          variables: { ...todo },
          // APIからのレスポンスが返ってくるまえにpropsに反映する値を設定
          optimisticResponse: () => ({ addTodo: { ...todo, __typename: 'Todo' } })
        })
      }
    }),
    options: {
      // 追加の後に全件リストを更新するアクション
      refetchQueries: [{ query: QueryGetTodos }],
      update: (proxy, { data: { addTodo } }) => {
        const query = QueryGetTodos;
        const data = proxy.readQuery({ query });

        data.getTodos.push(addTodo);

        proxy.writeQuery({ query, data });
      }
    }
  }),
  // 状態更新(チェック)Mutation
  graphql(UpdateTodoMutation, {  // あらかじめ定義したGraphQLクエリを使用
    props: (props) => ({
      onCheck: (todo) => {
        props.mutate({
          variables: { id: todo.id, title: todo.title, description: todo.description, completed: !todo.completed },
          // APIからのレスポンスが返ってくるまえにpropsに反映する値を設定
          optimisticResponse: () => ({ updateTodo: { id: todo.id, title: todo.title, description: todo.description, completed: !todo.completed, __typename: 'Todo' } })
        })
      }
    }),
    options: {
      // 更新の後に全件リストを更新するアクション
      refetchQueries: [{ query: QueryGetTodos }],
      update: (proxy, { data: { updateTodo } }) => {
        const query = QueryGetTodos;
        const data = proxy.readQuery({ query });

        data.getTodos = data.getTodos.map(todo => todo.id !== updateTodo.id ? todo : { ...updateTodo });

        proxy.writeQuery({ query, data });
      }
    }
  }),
  // 削除Mutation
  graphql(DeleteTodoMutation, {  // あらかじめ定義したGraphQLクエリを使用
    props: (props) => ({
      onDelete: (todo) => props.mutate({
        variables: { id: todo.id },
        // APIからのレスポンスが返ってくるまえにpropsに反映する値を設定
        optimisticResponse: () => ({ deleteTodo: { ...todo, __typename: 'Todo' } }),
      })
    }),
    options: {
      // 削除の後に全件リストを更新するアクション
      refetchQueries: [{ query: QueryGetTodos }],
      update: (proxy, { data: { deleteTodo: { id } } }) => {
        const query = QueryGetTodos;
        const data = proxy.readQuery({ query });

        data.getTodos = data.getTodos.filter(todo => todo.id !== id);

        proxy.writeQuery({ query, data });
      }
    }
  })
)(TodoList);

それ以外の部分は次の通りです。

TodoList.js
class TodoList extends Component {

  constructor(props) {
    super(props);
    this.state = {
      todo: {
        title: '',
        description: ''
      },
    };
  }

  // propsの初期値を設定
  static defaultProps = {
      todos: [],
      onAdd: () => null,
      onDelete: () => null,
      onUpdate: () => null,
  }

  todoForm = () => (
    <div>
      <span><input type="text" placeholder="タイトル" value={this.state.todo.title} onChange={this.handleChange.bind(this, 'title')} /></span>
      <span><input type="text" placeholder="説明" value={this.state.todo.description} onChange={this.handleChange.bind(this, 'description')} /></span>
      <button onClick={this.handleOnAdd}>追加</button>
    </div>
  );

  renderTodo = (todo) => (
    <li key={todo.id}>
      <input type="checkbox"checked={todo.completed} onChange={this.handleCheck.bind(this, todo)} />
      {!todo.completed && todo.title}
      {todo.completed && (<s>{todo.title}</s>)}
      <button onClick={this.handleOnDelete.bind(this, todo)}>削除</button>
    </li>
    );

  handleChange = (field, { target: { value }}) => {
    const { todo } = this.state;
    todo[field] = value;
    this.setState({ todo });
  }

  handleOnAdd = () => {
    if (!this.state.todo.title || !this.state.todo.description) {
      return;
    }
    const uuid = uuidv4();
    const newTodo = {
      id: uuid,
      title: this.state.todo.title,
      description: this.state.todo.description,
      completed: false
    }
    this.props.onAdd(newTodo);

    const { todo } = this.state;
    todo.title = '';
    todo.description = '';
    this.setState({ todo });
  }

  handleCheck = (todo) => {
    this.props.onCheck(todo);
  }

  handleOnDelete = (todo) => {
    this.props.onDelete(todo);
  }

  render() {
    const { todos } = this.props;
    return (
      <Fragment>
        {this.todoForm()}
        <ul>
          {todos.map(this.renderTodo)}
        </ul>
      </Fragment>
    );
  }
}

参考

Building a ReactJS Client App -AWS AppSync
ReactとApolloを使ってGithub GraphQL APIを試してみる -Qiita
APOLLO CLIENT -Apollo

まとめ

クライアント画の実装に関しては、react-apolloに依存する部分が多く、AWS AppSyncとReactの組み合わせをガッツリ使っていこうとすると、react-apolloのヘルパーメソッドやキャッシュなどのオプション周りを詳しく見ていく必要があるかと思います。
(逆にそれ以外の部分は、aws-appsync-reactがよしなにやってくれるので、気にする必要はない)
今回は、GraphQLクエリにQueryとMutationのみを使用しましたが、機会があれば、Subscriptionを使ってリアルタイムでサーバー側と通信を行うパターンも試してみたいと思います。