[AWS Amplify + React] AWS Amplifyで簡単な掲示板を作ってみる

2021-08-22AWS Amplify,IT記事AWS Amplify,React.js

AmplifyのNext.jsへの対応が進んでいる。NextのSSRやさらにISRに対応したらしく話題に(?)なっている。

試してみようと思ったが自分はNext関連は全く触ったことがない。なので前回のコードをReactで書いてみて次回Nextでなにか作るというようにゆっくりとやってみようと思う。上手く書けているかまだわからないがやってみよう。

プロジェクト作成

Reactプロジェクトを作るツールを使う。(npxはnodeに同封されているコマンドで、使うとインターネット上から指定したモジュールを探してきてインストールして使う。)

npx create-react-app tokumei_react_amplify

移動してnpm startとするとlocalhost:3000でプロジェクトが立ち上がる

cd tokume_react_amplify
npm start

サンプルコードを書き換えてHelloWorldしてみる。

import { Hello } from "./components/Hello.js";
function App() {
  return (
    <div className="App">
      <Hello />
    </div>
  );
}

export default App;
export function Hello() {
  return <div>Hello!</div>;
}

できた。

Reactはexport functionという部分からわかるようにclassではなく関数ベースにして、hookというスタイルで書くのが主流だ。

amplify init でプロジェクトのamplifyを設定する。

amplify init

Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project tokumeireactamplify
The following configuration will be applied:

Project information
| Name: tokumeireactamplify
| Environment: dev
| Default editor: Visual Studio Code
| App type: javascript
| Javascript framework: react
| Source Directory Path: src
| Distribution Directory Path: build
| Build Command: npm.cmd run-script build
| Start Command: npm.cmd run-script start

? Initialize the project with the above configuration? Yes

Githubのレポジトリと連帯する

次はgit initしてgithubに登録して、Amplify Consoleからgithubを連帯して、要はpushしたらデプロイするようにしよう。

ブラウザでAmplify Consoleにアクセスする。リージョンを合わせる。作ったアプリが左上にあるので、選択して、frontend environmentsからgithubを選択。

githubと認証して、レポジトリを選択して、環境をdev、デプロイするロールを選択して、あとは適当にOKしていくとデプロイされる。

Amplify Libraryをインストール

amplify libraryをインストールしていく

npm install aws-amplify @aws-amplify/ui-react

index.jsで読み込む

import Amplify from "aws-amplify";
import awsExports from "./aws-exports";
Amplify.configure(awsExports);

Graph QLスキーマを書く

amplify add api とするとプロジェクトにGraphQLでDynamoDBなAPIを作成できる。

amplify add api

適当に答えていくとデフォルトでTodoか、Blogというスキーマを生成してくれる。

ここからデフォルトGraphQLスキーマを変更していく。

その前にauto-transformerをインストールする。前回記事見て頂いて。

このようなカスタムdirectiveを使う場合、ビルドコマンドを書き換える必要がある。

ブラウザからアクセスしてビルドコマンドを書き換える。reactの場合以下のようになった。

(ちなみに以下のようなビルドコマンドをamplify.ymlという名前でプロジェクトのルートに置いておくとamplify consoleは勝手に認識して実行する)

version: 1
backend:
  phases:
    build:
      commands:
        - npm install -g graphql-auto-transformer
        - amplifyPush --simple
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - npm run build
  artifacts:
    baseDirectory: build
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*

ではGraphQLのスキーマ部分

type Thread
  @model
  @key(
    name: "byTypeCreatedAt"
    fields: ["type", "createdAt"]
    queryField: "byCreatedAt"
  ) {
  id: ID!
  type: String!
  title: String!
  comments: [Comment] @connection(keyName: "byThread", fields: ["id"])
  createdAt: AWSDateTime! @auto
}

type Comment @model @key(name: "byThread", fields: ["threadId", "createdAt"]) {
  id: ID!
  title: String!
  threadId: ID!
  createdAt: AWSDateTime! @auto
}

type Subscription {
  onCommentByThreadId(threadId: ID!): Comment
    @aws_subscribe(mutations: ["createComment"])
}

・ThreadとComment型を作っている。スレッドがコメントのリストを持っている関係。

・上でインストールした@autoを使っている

・ちょっと前に記事にしたcreatedAtでソートするようにしている。

・スレッド毎のサブスクリプションにするためにtype Subscritptionで定義してある。

次はreactのコードを書いていこう。

Reactコード部分

メインページを開くとスレッドの一覧があって、スレッドをクリックするとスレッドページに移動するようにする。

react routerをインストールしておいて

npm install react-router-dom

App.js部分

import { Index } from "./components/Index.js";
import { Thread } from "./components/Thread.js";
import { BrowserRouter, Switch, Route } from "react-router-dom";

function App() {
  return (
    <div className="App">
      <BrowserRouter>
        <Switch>
          <Route path="/" exact children={<Index />} />
          <Route path="/:threadId" children={<Thread />} />
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

スレッド一覧ページ部分

import React, { useEffect, useState } from "react";
import { API, graphqlOperation } from "aws-amplify";
import { byCreatedAt } from "../graphql/queries.js";
import { createComment, createThread } from "../graphql/mutations.js";

export function Index() {
  const formFirstValue = { title: "", firstComment: "" };
  const [formState, setFormState] = useState(formFirstValue);
  const [threadList, setThreadList] = useState([]);

  useEffect(() => {
    fetchThreadList();
  }, []);

  // 起動時スレッドの一覧を取得
  const fetchThreadList = async () => {
    const { data } = await API.graphql(
      graphqlOperation(byCreatedAt, {
        type: "t",
        sortDirection: "DESC",
      })
    );
    console.log(data);
    setThreadList(data.byCreatedAt.items);
  };

  // フォームをクリック時の動作
  const onSubmitForm = async (e) => {
    e.preventDefault();
    // フォームが空なら終了
    if (!formState.title || !formState.firstComment) return;
    const newThread = await createNewThread();
    await createFirstComment(newThread.id);
    setThreadList([newThread, ...threadList]);
    setFormState(formFirstValue);
  };

  // スレッドの作成
  const createNewThread = async () => {
    const newThread = { title: formState.title, type: "t" };
    const { data } = await API.graphql(
      graphqlOperation(createThread, { input: newThread })
    );
    console.log(data);
    return data.createThread;
  };

  // 最初のコメントを作成
  const createFirstComment = async (threadId) => {
    const newComment = { threadId: threadId, title: formState.firstComment };
    const { data } = await API.graphql(
      graphqlOperation(createComment, { input: newComment })
    );
    console.log(data);
  };

  // フォームに入力時、formStateを更新する
  const onChange = (event, propaty) => {
    setFormState({ ...formState, [propaty]: event.target.value });
  };

  return (
    <div>
      <div>匿名掲示板(Amplify + React)</div>
      <div>
        {threadList.map((thread, index) => {
          return (
            <div key={index}>
              <a href={thread.id}>{thread.title}</a>
            </div>
          );
        })}
      </div>
      <div>
        <form>
          <div>
            <div>
              <label htmlFor="threadTitle">スレッドタイトル</label>
            </div>
            <input
              id="threadTitle"
              value={formState.title}
              onChange={(event) => onChange(event, "title")}
            ></input>
          </div>
          <div>
            <div>
              <label htmlFor="threadFirstComment">最初のコメント</label>
            </div>
            <input
              id="threadFirstComment"
              value={formState.firstComment}
              onChange={(event) => onChange(event, "firstComment")}
            ></input>
          </div>
          <button onClick={onSubmitForm}>送信</button>
        </form>
      </div>
    </div>
  );
}

・スレッドをapiで作ったあと、そのままブラウザ上で続けて最初コメントを作るようにしている。サーバー側でしたい場合lambdaなどを使う。

スレッドページ部分

import { API, graphqlOperation } from "aws-amplify";
import { useEffect, useState, useRef } from "react";
import { useParams } from "react-router-dom";
import { createComment } from "../graphql/mutations";
import { getThread } from "../graphql/queries";
import { onCommentByThreadId } from "../graphql/subscriptions";

export function Thread() {
  const { threadId } = useParams();
  const [thread, setThread] = useState({
    title: "",
    comments: { items: [] },
  });
  const [commentForm, setCommentForm] = useState({ title: "" });
  const subscriptionRef = useRef();

  useEffect(() => {
    fetchThread();
    createSubscription();
    return () => {
      if (subscriptionRef) subscriptionRef.current.unsubscribe();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // スレッド情報を取得
  const fetchThread = async () => {
    console.log(threadId);
    const { data } = await API.graphql(
      graphqlOperation(getThread, { id: threadId })
    );
    console.log(data);
    setThread(data.getThread);
  };

  // subscription
  const createSubscription = () => {
    subscriptionRef.current = API.graphql(
      graphqlOperation(onCommentByThreadId, { threadId: threadId })
    ).subscribe({
      next: (data) => {
        console.log(data);
        addComment(data.value.data.onCommentByThreadId);
      },
      error: (error) => console.warn(error),
    });
  };

  // コメントのリストに新たに追加
  const addComment = (newComment) => {
    setThread((thread) => {
      // リスト内に既にあったら追加しない
      const idx = thread.comments.items.findIndex(
        (comment) => comment.id === newComment.id
      );
      if (idx !== -1) return thread;
      console.log("update comment list", thread);
      return {
        title: thread.title,
        comments: { items: [...thread.comments.items, newComment] },
      };
    });
  };

  // フォーム送信時
  const onSubmit = async (e) => {
    e.preventDefault();
    if (!commentForm.title) return;
    const newComment = await createNewComment();
    setCommentForm({ title: "" });
    setThread((thread) => {
      return {
        title: thread.title,
        comments: { items: [...thread.comments.items, newComment] },
      };
    });
  };

  // コメントの作成
  const createNewComment = async () => {
    const { data } = await API.graphql(
      graphqlOperation(createComment, {
        input: { threadId: threadId, title: commentForm.title },
      })
    );
    console.log(data);
    return data.createComment;
  };

  return (
    <div>
      <h1>{thread.title}</h1>
      {thread.comments.items.map((comment, idx) => {
        return <div key={idx}>{comment.title}</div>;
      })}
      <div>
        <form>
          <div>
            <label htmlFor="commentForm">コメント</label>
          </div>
          <input
            id="commentForm"
            value={commentForm.title}
            onChange={(event) => setCommentForm({ title: event.target.value })}
          ></input>
          <button onClick={onSubmit}>送信</button>
        </form>
      </div>
    </div>
  );
}

・subscriptionで新しいコメントが書かれたら受信するようにしてある。

リダイレクト設定

gitでgithubにpushするとデプロイされる。

アクセスしてリンクをクリックする。そのままだとhtmlがないといったエラーがでる。SPAの場合リダイレクト設定をする必要がある。

前回記事見て頂いて。

完成

サイトアドレス : https://main.d2b6blj4k8nlnq.amplifyapp.com/

githubレポジトリ : https://github.com/opvelll/tokumei_react_amplify

とりあえずここまで。Reactはシンプルに書ける印象を持った。しかしまだまだ細かいサイクルがわからない。useEffectとか。hookに関して言えば後発のVue3のCompositionAPIのほうがまとまっている。