[AWS Amplify + React] AWS Amplifyで簡単な掲示板を作ってみる
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のほうがまとまっている。
ディスカッション
コメント一覧
まだ、コメントがありません