AWS
TypeScript
React
amplify
AppSync

React+Amplify+AppSync+TypeScriptでリアルタイム掲示板アプリを作る

amplify_react_ts.png

この記事は、「【爆速】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

以下のスキーマのサンプルが出来るので、そのまま使います。


amplify/backend/api/schema.graphql

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の初期化部分です。


src/index.tsx

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();


次に、アプリ本体です。ポイントは後ほど解説します。

また、流れを掴む事が目的のため、エラー処理は入れてません。


src/App.tsx

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

board.png

複数画面開くと、同時にリアルタイムで更新されます。


解説と感想


モデル

graphqlのスキーマに対応した型がsrc/API.tsに自動生成されているので、基本的にここに定義されている型を使います。


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の中身のみの型が無かったので、以下のように独自に定義しています。


src/App.tsx

type Post = {

id: string;
title: string;
content: string;
price: number | null;
rating: number | null;
};

GraphQLのスキーマそのままなので、自動生成されて欲しい気もします。


登録

追加ボタンを押したときに呼ばれるメソッドです。


src/App.tsx

  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を使います。


src/graphql/mutations.ts

export const createPost = `mutation CreatePost($input: CreatePostInput!) {

createPost(input: $input) {
id
title
content
price
rating
}
}
`
;

追加するときの引数$input: CreatePostInput!に対応する型も自動生成されているので、これに登録したいデータを設定してクエリを送信するだけです。


src/API.ts

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一覧の更新を伝えます。


src/App.tsx

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とも連携できるようなので、色々と応用も出来て便利そうです。