Help us understand the problem. What is going on with this article?

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした