この記事は、「【爆速】React+Amplify+AppSyncでリアルタイム掲示板アプリを15分で作り上げる 〜これが最高のDeveloper Experienceだ〜 - Qiita」を参考にさせて頂きました。
Amplifyのコマンドでコードを自動生する際にTypeScriptを選択できるようなので、どんな感じなのか試してみました。
ついでに、ReactのHooksも使ってます。
バージョン
使用した環境は以下のとおりです。
$ create-react-app --version
3.0.1
$ node -v
v8.15.1
$ npm -v
6.9.0
$ amplify --version
1.7.0
自分の環境にはamplifyのコマンドすら入っていない状態だったので、公式のページを見てインストールしました。
Getting Started · Create React App
Reactアプリの雛形を作る
create-react-app
で引数に--typescript
を指定して作成し、amplify init
で初期化設定をしていきます。
profile
の指定等は適宜変更して下さい。
$ create-react-app boardapp --typescript
$ cd boardapp
$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project boardapp
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path: src
? Distribution Directory Path: build
? Build Command: npm run-script build
? Start Command: npm run-script start
? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use default
GraphQLのAPIを追加します。
$ amplify add api
? Please select from one of the below mentioned services GraphQL
? Provide API name: boardapp
? Choose an authorization type for the API API key
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? No
? Provide a custom type name Post
以下のスキーマのサンプルが出来るので、そのまま使います。
type Post @model {
id: ID!
title: String!
content: String!
price: Int
rating: Float
}
次に、デプロイとクライアントのコードの自動生成をします。
$ amplify push
? Are you sure you want to continue? Yes
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target typescript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.ts
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
? Enter the file name for the generated code src/API.ts
ここまで終わると、GraplQLのAPIがAWSにデプロイされ、ローカルのディレクトリは以下のような構成になりました。
$ tree -L 5 -I "node_modules"
.
├── README.md
├── amplify
│ ├── #current-cloud-backend
│ │ ├── amplify-meta.json
│ │ ├── api
│ │ │ └── boardapp
│ │ │ ├── build
│ │ │ ├── parameters.json
│ │ │ ├── resolvers
│ │ │ ├── schema.graphql
│ │ │ └── stacks
│ │ └── backend-config.json
│ ├── backend
│ │ ├── amplify-meta.json
│ │ ├── api
│ │ │ └── boardapp
│ │ │ ├── build
│ │ │ ├── parameters.json
│ │ │ ├── resolvers
│ │ │ ├── schema.graphql
│ │ │ └── stacks
│ │ ├── awscloudformation
│ │ │ └── nested-cloudformation-stack.yml
│ │ └── backend-config.json
│ └── team-provider-info.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── API.ts
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── aws-exports.js
│ ├── graphql
│ │ ├── mutations.ts
│ │ ├── queries.ts
│ │ ├── schema.json
│ │ └── subscriptions.ts
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ └── serviceWorker.ts
├── tsconfig.json
└── yarn.lock
amplifyのパッケージ追加
yarnでパッケージを登録します。
TypeScriptの型も一緒にに登録されるようです。
$ yarn add aws-amplify aws-amplify-react
アプリケーションの更新
create-react-app
で自動生成されたコードを変更していきます。
まず、Amplifyの初期化部分です。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import Amplify from "aws-amplify" // 追加
import config from "./aws-exports" // 追加
Amplify.configure(config) // 追加
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
次に、アプリ本体です。ポイントは後ほど解説します。
また、流れを掴む事が目的のため、エラー処理は入れてません。
import React, { useEffect, useState } from "react";
import { API, graphqlOperation } from "aws-amplify";
import { listPosts } from "./graphql/queries";
import { createPost } from "./graphql/mutations";
import { onCreatePost } from "./graphql/subscriptions";
import {
ListPostsQuery,
OnCreatePostSubscription,
CreatePostMutationVariables
} from "./API";
type Post = {
id: string;
title: string;
content: string;
price: number | null;
rating: number | null;
};
type FormState = {
title: string;
content: string;
};
type PostSubscriptionEvent = { value: { data: OnCreatePostSubscription } };
const usePosts = () => {
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
(async () => {
// 最初のPost一覧取得
const result = await API.graphql(graphqlOperation(listPosts));
if ("data" in result && result.data) {
const posts = result.data as ListPostsQuery;
if (posts.listPosts) {
setPosts(posts.listPosts.items as Post[]);
}
}
// Post追加イベントの購読
const client = API.graphql(graphqlOperation(onCreatePost));
if ("subscribe" in client) {
client.subscribe({
next: ({ value: { data } }: PostSubscriptionEvent) => {
if (data.onCreatePost) {
const post: Post = data.onCreatePost;
setPosts(prev => [...prev, post]);
}
}
});
}
})();
}, []);
return posts;
};
const App: React.FC = () => {
const [input, setInput] = useState<FormState>({
title: "",
content: ""
});
const posts = usePosts();
const onFormChange = ({
target: { name, value }
}: React.ChangeEvent<HTMLInputElement>) => {
setInput(prev => ({ ...prev, [name]: value }));
};
const onPost = () => {
if (input.title === "" || input.content === "") return;
const newPost: CreatePostMutationVariables = {
input: {
title: input.title,
content: input.content
}
};
setInput({ title: "", content: "" });
API.graphql(graphqlOperation(createPost, newPost));
};
return (
<div className="App">
<div>
タイトル
<input value={input.title} name="title" onChange={onFormChange} />
</div>
<div>
内容
<input value={input.content} name="content" onChange={onFormChange} />
</div>
<button onClick={onPost}>追加</button>
<div>
{posts.map(data => {
return (
<div key={data.id}>
<h4>{data.title}</h4>
<p>{data.content}</p>
</div>
);
})}
</div>
</div>
);
};
export default App;
あとは、起動するだけです。
$ yarn start
複数画面開くと、同時にリアルタイムで更新されます。
解説と感想
モデル
graphqlのスキーマに対応した型がsrc/API.ts
に自動生成されているので、基本的にここに定義されている型を使います。
export type ListPostsQuery = {
listPosts: {
__typename: "ModelPostConnection",
items: Array< {
__typename: "Post",
id: string,
title: string,
content: string,
price: number | null,
rating: number | null,
} | null > | null,
nextToken: string | null,
} | null,
};
export type OnUpdatePostSubscription = {
onUpdatePost: {
__typename: "Post",
id: string,
title: string,
content: string,
price: number | null,
rating: number | null,
} | null,
};
Postの中身のみの型が無かったので、以下のように独自に定義しています。
type Post = {
id: string;
title: string;
content: string;
price: number | null;
rating: number | null;
};
GraphQLのスキーマそのままなので、自動生成されて欲しい気もします。
登録
追加ボタンを押したときに呼ばれるメソッドです。
const onPost = () => {
if (input.title === "" || input.content === "") return;
const newPost: CreatePostMutationVariables = {
input: {
title: input.title,
content: input.content
}
};
setInput({ title: "", content: "" });
API.graphql(graphqlOperation(createPost, newPost));
};
GraphQLの代表的なクエリが自動生成されているので、graphqlOperation
に指定することで、クエリの種類を切り替えられます。ここでは新規登録なので、createPost
を使います。
export const createPost = `mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
content
price
rating
}
}
`;
追加するときの引数$input: CreatePostInput!
に対応する型も自動生成されているので、これに登録したいデータを設定してクエリを送信するだけです。
export type CreatePostInput = {
id?: string | null,
title: string,
content: string,
price?: number | null,
rating?: number | null,
};
export type CreatePostMutationVariables = {
input: CreatePostInput,
};
一覧取得とデータ登録の監視
登録されたデータの一覧取得と追加されたデータの監視は、カスタムフックを作って実現しています。
useEffect
でコンポーネントのマウント時に、Postの一覧取得、Post作成の購読を追加を順番に行い、useState
で作成したPost一覧を戻り値として返す事で、Post一覧の更新を伝えます。
type PostSubscriptionEvent = { value: { data: OnCreatePostSubscription } };
const usePosts = () => {
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
(async () => {
// 最初のPost一覧取得
const result = await API.graphql(graphqlOperation(listPosts));
if ("data" in result && result.data) {
const posts = result.data as ListPostsQuery;
if (posts.listPosts) {
setPosts(posts.listPosts.items as Post[]);
}
}
// Post追加イベントの購読
const client = API.graphql(graphqlOperation(onCreatePost));
if ("subscribe" in client) {
client.subscribe({
next: ({ value: { data } }: PostSubscriptionEvent) => {
if (data.onCreatePost) {
const post: Post = data.onCreatePost;
setPosts(prev => [...prev, post]);
}
}
});
}
})();
}, []);
return posts;
};
型の整合性を取るため、少しややこしいです。
API.graphql
の戻り値の型は Promise<GraphQLResult> | Observable<object>
となっています。
引数のgraphqlOperation
の内容によって戻り値の型が変わります。また、戻り値のデータ型がobject
で、ジェネリックで型の指定も出来ないので、所々if
で型を絞り込んだり、as
でキャストしてます。
もっとうまい使い方があるのかもしれませんが、もう少し使いやすくならないかなーと思いました。
さいごに
最初は全体のイメージが掴みづらかったのですが、実際に使ってみると
思ったよりも簡単にGraphQLのAPIが作れました。
DynamoDB以外にもRDBやLambdaとも連携できるようなので、色々と応用も出来て便利そうです。