はじめに
前回の記事まででは、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
をダウンロードします。
ダウンロードしたファイルを、作成したプロジェクトのsrc
以下に配置し、他のパッケージと合わせて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による認証を使用します。
// 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を渡しています。
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
import gql from "graphql-tag";
export default gql(`
query {
getTodos {
id
title
description
completed
}
}`);
Todo作成のMutation
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
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
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を受け取ることができます。
const TodoListWithData = graphql(QueryGetTodos)(TodoList);
また、複数のQuery、MutationとComponentを連携する場合には、react-apolloのcomposeメソッドを使用します。
実際には、それぞれにオプションなどを指定するため、あくまでイメージです。
const TodoListWithData = compose(
graphql(QueryGetTodos),
graphql(MutationAddTodo),
graphql(MutationUpdateTodo),
graphql(MutationDeleteTodo),
)(TodoList);
実際に、それぞれのQuery、Mutationを紐付ける部分は次の通りです。
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);
それ以外の部分は次の通りです。
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を使ってリアルタイムでサーバー側と通信を行うパターンも試してみたいと思います。