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


## はじめに

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


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

### 雛形の作成

[Create React App](https://github.com/facebook/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`をダウンロードします。

<img width="1153" alt="スクリーンショット 2018-01-25 22.17.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/79414/d965285b-769d-495f-777c-d105a539a393.png">

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

```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による認証を使用します。


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

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

```js: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](https://www.npmjs.com/package/graphql-tag)のgqlメソッドを使って、クエリを定義します。今回は、`src/GraphQL`以下に作成しました。

### Todo全件取得のQuery

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

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

### Todo作成のMutation

```js: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

```js: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

```js: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](https://github.com/apollographql/react-apollo)を使用します。

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

```js:graphql
const TodoListWithData = graphql(QueryGetTodos)(TodoList);
```

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

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

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

```js: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);
```

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

```js: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 && (
{todo.title}
)}
      <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](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/building-a-client-app-react.html)
[ReactとApolloを使ってGithub GraphQL APIを試してみる -Qiita](https://qiita.com/bananaumai/items/2cd6d48df20b378a44f5)
[APOLLO CLIENT -Apollo](https://www.apollographql.com/docs/react/)

## まとめ

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