AWS Amplifyで簡単にToDoアプリ開発

AWS AmplifyとReactをつかってToDoタスクの管理アプリを作成してみました。簡単にそれっぽいものが作れるので、開発過程を記したいと思います。

# 背景

AWSに興味を持ち始めて、AWSを利用したアプリを開発したいなぁと思っていたある日、わが社に入社された新卒の方々が研修をしていました。
そこで、新卒の方が多忙な研修タスクを管理できるように、個人的に気になっていたAWS AmplifyをつかってToDoアプリを作ろうと思いました。

# AWS Amplifyとは
AWS Amplifyは、フロントエンドのウェブ/モバイルアプリケーションを構築するための開発プラットフォームです。様々なAWSサービスと連携して、バックエンドの複雑さを意識せずに簡単にアプリ開発・デプロイできます。
AWS Amplifyは、サービス・ツールで構成されています。今回主に利用するものについて記します。

## Amplify CLI

各種AWSサービスを簡単に利用できる、コマンドベースのツールチェーンです。クラウドバックエンドの構築や管理のほか、AWSサービスとの連携に必要な設定ファイルやソースコードを自動生成してくれます。
AWSサービスやバックエンドについてあまり知識がなくても、開発が可能です。

## Amplify Console

Amplify Consoleは、サーバーレスウェブアプリケーションを公開するための環境を、自動で構築してくれるサービスです。Gitリポジトリを紐付けておくことにより、簡単にデプロイ環境を構築してくれるほか、ホスティングまで自動実行してくれます。

- Amplify UI Components
入力フォームなどの機能をもつUIを埋め込むためのUIコンポーネントです。
今回はせっかくなので、積極的にこのコンポーネントを利用して画面UIを作っていきます。

- [公式サイト](https://aws.amazon.com/jp/amplify/#:~:text=AWS%20Amplify%20%E3%81%AF%E3%80%81%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89,%E3%81%AE%E5%B0%82%E9%96%80%E7%9F%A5%E8%AD%98%E3%81%AF%E4%B8%8D%E8%A6%81%E3%80%82)

## 料金
以下の制限で12か月の無料枠があるので、個人の開発なら簡単に試していただけます。
- 1,000ビルド/月
- 5GBのストレージおよび15GBのホスティング

無料枠を超えると、以下のように従量課金されます。
- ビルド&デプロイ
    - 0.01USD/ビルド分
- ホスティング
    - ストレージ1GBあたり0.023USD/月
    - ホスティングサービス1GBあたり0.15USD

# 開発手順

では、ここからは開発の手順を記していきます。

## Reactアプリの作成
まずは以下のコマンドを実行して、Reactアプリを作成します。
```
$ npx create-react-app todo
```
以下のコマンドで作成されたディレクトリに移動して、アプリを実行します。
```
$ cd todo
$ npm start
```
ブラウザが立ち上がり、以下の画面が表示されればOKです。
![localhost_3000_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/c50feca6-e940-533a-d8b7-a0b8ba23b732.png)

## GitHubのリポジトリへのアプリのプッシュ

今回はGitHubのリポジトリからAWS Amplifyにコードを接続するため、GitHubのリポジトリに作成したアプリをプッシュする必要があります。
GitHubにリポジトリを作成します。
`Repository Name`などを設定して、`Create repository`ボタンをクリックします。
![スクリーンショット 2023-06-11 222054.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/2f70429b-8cf3-bc29-652c-d3fc5a5ff115.png)

次に以下のコマンドを実行し、作成したReactアプリのGutを初期化します。
```
$ ls -al | grep git
$ rm -rf .git/
```

以下のコマンドを実行し、作成したリポジトリにアプリをプッシュします。
```
$ git init
$ git remote add origin https://github.com/*ユーザー名*/*リポジトリー名*.git
$ git add .
$ git commit -m "initial commit"
$ git push origin master
```

## AWS Amplifyでのアプリのデプロイ

[AWSマネジメントコンソール](https://aws.amazon.com/jp/console/)にログインし、AWS Amplifyにアクセスします。
まだAWS Amplifyを使用したことがない場合は、以下のような画面に遷移しますので、`ウェブアプリケーションをホスト`の`使用を開始する`をクリックします。
![スクリーンショット 2023-06-11 224557.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/608d20e7-a6c2-6297-4900-2edb80c39c04.png)

今回はGitHubのコードを接続するので、`GitHub`を選択して`続行`をクリックします。

すると、GitHubの認証を求められるので、画面指示に従って操作を行います。

認証に成功すると以下の画面に遷移します。`最近更新されたリポジトリ`で作成したリポジトリを選択し、`ブランチ`を選択したら`次へ`をクリックします。![スクリーンショット 2023-06-11 225246.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/a105636c-033f-be63-ee2f-55111f968563.png)

次に遷移した画面はそのまま`次へ`をクリックすると、確認画面に遷移します。
確認画面で`保存してデプロイ`をクリックすると、AWS Amplifyはアプリのプロビジョニング、ビルド、デプロイを開始します。

以下の画面のステータスバーで`デプロイ`が成功していれば、AWS Amplifyでのアプリのデプロイが完了です。
赤線で囲んだURLにアクセスすると、作成したアプリの画面が開きます。
![スクリーンショット 2023-06-11 231055.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/63e134d3-7d9b-2007-a4b8-bc7dd69ed9bd.png)

今後、コードを変更した場合、そのコードを`main`ブランチにプッシュすると、自動的に新しくビルドが開始され、アプリがデプロイされます。

## ローカルのAmplifyアプリの初期化

Amplifyプロジェクトで開発しながら、新しい機能を追加できるようにAmplifyプロジェクトをローカル環境に落とし込んでいきます。

以下のコマンドでAmplify CLIをインストールします。Amplify CLI を使用することで、ターミナルから AWSサービスを直接作成、管理、削除できます。
```
$ npm install -g @aws-amplify/cli
```

Amplify CLIのインストールが完了したら、Amplifyの設定を行います。以下のコマンドを実行します。
```
$ amplify configure
```
リージョンを入力すると、IAMユーザーの作成画面が表示されます。
IAMユーザーの作成画面では、ユーザー名を入力すればデフォルトで既存のポリシーが設定されているので、そのまま次へ進んでユーザーを作成してください。ユーザー作成後は、認証情報(CSV)をダウンロードして保存しておいてください。

その後ターミナルに戻り、ユーザー名を入力して、`Enter`キーを押します。
```
Specify the AWS Region
? region:  ap-northeast-1
Specify the username of the new IAM user:
? user name: xxxxxx
```

先ほど作成したアクセスキーIDとアクセスキーを入力してください。
```
Enter the access key of the newly created user:
? accessKeyId:  ********************
? secretAccessKey:  ****************************************
```

Amplifyの設定が完了したら、Amplifyアプリを初期化します。
AWSマネジメントコンソールで作成したAmplifyコンソールにアクセスします。

`Backend environments`タブをクリックし、`Get started`をクリックします。
バックエンドがデプロイされるので、完了するまで待ちます。
![スクリーンショット 2023-06-19 000732.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/eec5dd8d-1799-9bce-4899-8bb65f11c13b.png)

`Studioを起動する`をクリックすると、バックエンドを管理するためのビジュアルインターフェイスが別タブで開きます。
![スクリーンショット 2023-06-19 001045.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/04f80be1-a056-edb4-6405-8dbe87400f3b.png)

`Backend environments`タブに戻り、`ローカル設定手順`をクリックします。
表示されるコマンドをターミナルで実行します。
![スクリーンショット 2023-06-19 001712.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/46c8cc9f-5e36-f37e-eded-1c732cccdb55.png)

手順に従ってセットアップします。
```
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
? 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 plan on modifying this backend? Y
```

## 認証機能の追加
作成したアプリに認証機能を追加します。Amazon Cognitoを活用して、Amplify CLIとライブラリでユーザーを認証します。

### Amplifyライブラリのインストール
まず、以下を実行してAmplifyライブラリの`@aws-amplify/ui-react`をインストールします。
`aws-amplify`ライブラリには、さまざまなAWSサービスと対話するためのクライアント側のAPIがすべて含まれており、`@aws-amplify/ui-react`ライブラリにはフレームワーク固有のUIコンポーネントが含まれています。
```
$ npm install aws-amplify @aws-amplify/ui-react
```

### 認証サービスの作成
以下のコードを実行することで、Amplify CLIを利用して認証サービスを作成します。
```
$ amplify add auth

? Do you want to use the default authentication and security configuration? Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? No, I am done.
```

### 認証サービスの導入
次に、以下を実行して認証サービスをデプロイすれば、認証サービスを導入することができます。
```
$ amplify push --y
```

### アプリでの認証機能の利用
認証サービスが導入できたので、Reactのアプリを認証機能が利用できるように修正していきます。

以下のようにコードを修正します。
```
import logo from "./logo.svg";

// 以下が認証機能のためのインポート文
import "@aws-amplify/ui-react/styles.css";
import {
  withAuthenticator,
  Button,
  Heading,
  Image,
  View,
  Card,
} from "@aws-amplify/ui-react";

function App({ signOut }) {
  return (
    <View className="App">
      <Card>
        <Image src={logo} className="App-logo" alt="logo" />
        <Heading level={1}>We now have Auth!</Heading>
      </Card>
　　　// サインアウトボタンの追加
      <Button onClick={signOut}>Sign Out</Button>
    </View>
  );
}

// withAuthenticatorコンポーネントを利用
export default withAuthenticator(App);
```
`withAuthenticator`コンポーネントを使用することで、ユーザー認証フロー全体を足場にして、ユーザーがサインアップ、サインイン、パスワードのリセット、多要素認証 (MFA) のサインインの確認を行えるようにします。

以下を実行してアプリを起動します。
```
$ npm start
```

アプリを起動すると、以下のように認証画面が表示されるようになります。`Create Account`タブで情報を入力して`Create Account`ボタンをクリックすると入力したメールアドレスに確認コードが送信されます。確認コードを入力するとサインインできます。
![スクリーンショット 2023-06-19 213839.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/6c8b975b-abc5-e597-0f25-c3bb026789ff.png)

サインインできると、サインアウトボタンが表示されます。サインアウトボタンをクリックするとサインアウトでき、確認画面の`Sign In`タブから再度サインインできます。

Amplify Studioを起動して、再度メニュー`User management`をクリックすると、登録されたユーザーの情報を確認したり、ユーザーグループを設定したりできます。
`Authentication`メニューからは、パスワードポリシーの設定など認証機能の設定を行うことができます。
![スクリーンショット 2023-06-19 215335.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/fdc2adf3-b2c5-d584-d2be-06a7ec95c875.png)

## データベースの利用
AWS Amplifyでは、AWS AppSyncを使用するGraphQL APIを作成することでAmazon DynamoDBと連携することができます。
今回はAmazon DynamoDBでToDoタスクを追加、編集、削除できるようにしていきます。

### APIの追加
以下を実行して、GraphQL APIを追加します。
```
$ amplify add api

? Select from one of the below mentioned services: GraphQL
? Here is the GraphQL API that we will create. Select a setting to edit or continue: Continue
? Choose a schema template: Single object with fields (e.g., "Todo" with ID, name, description)
? Do you want to edit the schema now? (Y/n) yes
```

### データベースの作成 

 `/amplify/backend/api/<api_name>/schema.graphql`をエディターで開き、以下を追記して保存します。
ちなみにデータのスカラー型は[こちら](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/scalars.html)を参照ください。
```
type Todo @model @auth(rules: [{ allow: public }]) {
  id: ID! // 各ToDoタスクのID
  title: String! // タイトル
  status: String // ステータス
  priority: String // 優先度
  start: AWSDate // マイルストーンの開始日
  end: AWSDate // マイルストーンの終了日
  description: String　// 説明
  user: String! // 登録したユーザーのID
}
```

### APIのデプロイ
以下を実行することで、以下の3つが行われます。
1. AWS AppSync API を作成する
1. DynamoDB テーブルを作成する
1. API のクエリに使用できるローカルGraphQLオペレーションを`src/graphql`にあるフォルダーに作成する
```
$ amplify push --y
```

## APIを利用したアプリ画面の作成

GraphQL APIを追加してデータベースを操作できるようになったので、次はアプリ画面を作成していきます。
今回はReact用に様々なUIコンポーネントが用意されている`@aws-amplify/ui-react`を利用して、画面を作成していきます。

### データの表示

APIを利用して取得したデータを表示するようなアプリ画面を作成していきます。
利用するコンポーネントは認証で利用した`withAuthenticator`も含めて、以下を利用します
Amplify UI Componentsの詳細は[こちら](https://ui.docs.amplify.aws/?platform=react)を参照ください。
- withAuthenticator,
- Button
- Flex
- Table
- TableCell
- TableBody
- TableHead
- TableRow
- Heading
- View

では、画面表示のコードを修正していきます。
`src/App.js`を以下のように修正します。

```js:src/App.js
import "./App.css";
import { Amplify, API } from "aws-amplify";
import config from "./aws-exports";
import "@aws-amplify/ui-react/styles.css";
import {
  withAuthenticator,
  Button,
  Flex,
  Table,
  TableCell,
  TableBody,
  TableHead,
  TableRow,
  Heading,
  View,
} from "@aws-amplify/ui-react";
import { listTodos } from "./graphql/queries";
import React, { useState, useEffect } from "react";

Amplify.configure(config);

function App({ signOut }) {
  const [todos, setTodos] = useState([]);

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

  async function fetchTodos() {
    const apiData = await API.graphql({ query: listTodos });
    const todosFromAPI = apiData.data.listTodos.items;
    setTodos(todosFromAPI);
  }

  return (
    <View className="App" margin="0 3rem">
      <View className="header">
        <Flex
          direction="row"
          justifyContent="space-between"
          alignItems="center"
        >
          <Heading level={1}>My Todo App</Heading>
          <Flex direction="row">
            <Button onClick={signOut}>Sign Out</Button>
          </Flex>
        </Flex>
      </View>

      <View margin="3rem 0">
        <Table caption="" highlightOnHover={false}>
          <TableHead>
            <TableRow>
              <TableCell as="th">No.</TableCell>
              <TableCell as="th">Title</TableCell>
              <TableCell as="th">Status</TableCell>
              <TableCell as="th">Priority</TableCell>
              <TableCell as="th">Milestone</TableCell>
              <TableCell as="th">Discription</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {todos.map((todo, index) => (
              <TableRow key={index}>
                <TableCell>{index}</TableCell>
                <TableCell>{todo.title}</TableCell>
                <TableCell>{todo.status}</TableCell>
                <TableCell>{todo.priority}</TableCell>
                <TableCell>
                  {todo.start} - {todo.end}
                </TableCell>
                <TableCell>
                  {todo.description ? todo.description : "-"}
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </View>
    </View>
  );
}

export default withAuthenticator(App);
```

以下のインポート文でAPI、およびGraphQLのクエリを利用できるようにします。
```
import { Amplify, API } from "aws-amplify";
import { listTodos } from "./graphql/queries";
```

`fetchTodos`関数は、以下の処理でAPIを利用して登録されているすべてのToDoタスクの情報を取得しています。取得したデータは変数`todos`にセットします。

```
await API.graphql({ query: listTodos });
```

`todos`のデータは`Table`などのコンポーネントで表形式で表示されます。Dynamo DBにToDoタスクのデータがあれば、画面にToDoタスクのデータが表示されます。

アプリを起動してログインすると、以下のような画面が表示されます。
![スクリーンショット 2023-06-24 160437.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/6afe4385-eece-4996-f24b-518116905b1c.png)

### データの追加・削除

データが表示できるようになりましたので、画面からデータを登録できるようにしています。
今回は、画面上の`Add+`ボタンをクリックすると、ToDoタスクを追加するためのモーダルが表示されるようにします。(モーダルにするのは筆者の好みです)

モーダル表示のため`src/index.css`を以下のように修正します。
```css:src/index.css
.overlay {
  overflow: scroll;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  padding-top: 15rem;
}

.content {
  overflow: auto;
  z-index: 2;
  width: 50%;
  padding: 1em;
  background: #fff;
  border-radius: 4px;
}
```

ToDoタスクの追加モーダルのコンポーネント`AddTodoModal`を作成します。
`src`ディレクトリ下に`components`ディレクトリを作成します。
`src/components/AddTodoModal.js`を作成し、以下のように編集します。

```
import React, { useState } from "react";
import {
  Button,
  Flex,
  Heading,
  SelectField,
  TextField,
  TextAreaField,
  View,
} from "@aws-amplify/ui-react";
import { DatePicker } from "@mui/x-date-pickers";
import dayjs from "dayjs";

const statusList = ["New", "Open", "On Hold", "Resolved", "Close"];
const priorityList = ["Trivial", "Minor", "Major", "Critical", "Blocker"];

export function AddTodoModal({ userId, createTodo, handleAddModal }) {
  const [todo, setTodo] = useState({
    user: userId,
    title: undefined,
    status: statusList[0],
    priority: priorityList[0],
    start: undefined,
    end: undefined,
    description: undefined,
  });

  return (
    <div className="overlay">
      <div className="content">
        <View className="header">
          <Heading level={2}>Add Todo</Heading>
        </View>
        <View margin="1rem">
          <Flex direction="column">
            <TextField
              placeholder="Input todo title"
              label="Title"
              onChange={(i) => {
                setTodo({ ...todo, title: i.target.value });
              }}
            />
            <TextAreaField
              label="Discription"
              placeholder="Input todo discription"
              onChange={(i) => {
                setTodo({ ...todo, description: i.target.value });
              }}
            />
            <SelectField
              label="Status"
              onChange={(i) => {
                setTodo({ ...todo, status: i.target.value });
              }}
            >
              {statusList.map((item, i) => (
                <option key={i} value={item}>
                  {item}
                </option>
              ))}
            </SelectField>
            <SelectField
              label="Priority"
              onChange={(i) => {
                setTodo({ ...todo, priority: i.target.value });
              }}
            >
              {priorityList.map((item, i) => (
                <option key={i} value={item}>
                  {item}
                </option>
              ))}
            </SelectField>
            <Flex
              direction="row"
              justifyContent="space-between"
              alignItems="center"
              margin="1rem 0"
            >
              <DatePicker
                label="Start Date"
                onChange={(i) => {
                  setTodo({ ...todo, start: dayjs(i).format("YYYY-MM-DD") });
                }}
              />
              <span>-</span>
              <DatePicker
                label="End Date"
                onChange={(i) => {
                  setTodo({ ...todo, end: dayjs(i).format("YYYY-MM-DD") });
                }}
              />
            </Flex>
          </Flex>
          <Flex
            direction="row"
            justifyContent="flex-end"
            alignItems="center"
            margin="1rem 0"
          >
            <Button onClick={handleAddModal}>Cansel</Button>
            <Button
              onClick={() => {
                createTodo(todo);
                handleAddModal();
              }}
            >
              Add
            </Button>
          </Flex>
        </View>
      </div>
    </div>
  );
}
```
`AddTodoModal`コンポーネントには以下を引数として渡す必要があります。
- userId: ログインしているユーザーの情報
- createTodo: APIを利用してデータを追加する関数
- handleAddModal: 追加モーダルの表示を切り替える関数

`AddTodoModal`コンポーネントの入力フォームでは、`@aws-amplify/ui-react`の以下のコンポーネントを利用しています。
- SelectField
- TextField
- TextAreaField

マイルストーンを設定するための日付選択コンポーネントは、まだ`@aws-amplify/ui-react`になさそうだったので、`@mui/x-date-pickers`をインストールして`DatePicker`コンポーネントを使いました。
`DatePicker`コンポーネントを利用するために、以下をインストールしてコンポーネントを設定する必要がありますので、とりあえず以下を実行して必要なライブラリをインストールしましょう。
```
$ npm install @mui/x-date-pickers
$ npm install dayjs
$ npm install @mui/material @emotion/react @emotion/styled
$ npm install @mui/styled-engine-sc styled-components
```

一旦、`AddTodoModal`コンポーネントが作成できました。

次にAPIを利用してデータの追加、削除できるように`src/App.js`を以下のように修正します。

```js:src/App.js
import "./App.css";
import { Amplify, Auth, API } from "aws-amplify";
import config from "./aws-exports";
import "@aws-amplify/ui-react/styles.css";
import {
  withAuthenticator,
  Button,
  Flex,
  Table,
  TableCell,
  TableBody,
  TableHead,
  TableRow,
  Heading,
  View,
} from "@aws-amplify/ui-react";
import { listTodos } from "./graphql/queries";
import {
  createTodo as createTodoMutation,
  deleteTodo as deleteTodoMutation,
} from "./graphql/mutations";
import React, { useState, useEffect } from "react";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AddTodoModal } from "./components/AddTodoModal";

Amplify.configure(config);

function App({ signOut }) {
  const [user, setUser] = useState({ id: "", name: "" });
  const [todos, setTodos] = useState([]);
  const [isOpenAddModal, setIsOpenAddModal] = useState(false);

  useEffect(() => {
    Auth.currentAuthenticatedUser().then((userInfo) => {
      setUser({ id: userInfo.attributes.sub, name: userInfo.username });
    });
  }, []);

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

  async function fetchTodos() {
    const apiData = await API.graphql({ query: listTodos });
    const todosFromAPI = apiData.data.listTodos.items;
    setTodos(todosFromAPI);
  }

  function handleAddModal() {
    if (isOpenAddModal === false) {
      setIsOpenAddModal(true);
    } else if (isOpenAddModal === true) {
      setIsOpenAddModal(false);
    }
  }

  async function createTodo(event) {
    const data = {
      title: event.title,
      status: event.status,
      priority: event.priority,
      start: event.start,
      end: event.end,
      description: event.description,
      user: event.user,
    };
    await API.graphql({
      query: createTodoMutation,
      variables: { input: data },
    });
    fetchTodos();
  }

  async function deleteTodo({ id }) {
    const newTodos = todos.filter((todo) => todo.id !== id);
    setTodos(newTodos);
    await API.graphql({
      query: deleteTodoMutation,
      variables: { input: { id } },
    });
  }

  return (
    <LocalizationProvider dateAdapter={AdapterDayjs}>
      <View className="App" margin="0 3rem">
        <View className="header">
          <Flex
            direction="row"
            justifyContent="space-between"
            alignItems="center"
          >
            <Heading level={1}>My Todo App</Heading>
            <Flex direction="row">
              <p>{user.name}</p>
              <Button onClick={signOut}>Sign Out</Button>
            </Flex>
          </Flex>
        </View>
        <View className="add-todo-button" margin="1rem 1rem">
          <Flex
            direction="row"
            justifyContent="space-between"
            alignItems="center"
          >
            <Button onClick={handleAddModal}>Add +</Button>
          </Flex>
        </View>
        {isOpenAddModal === true && (
          <AddTodoModal
            userId={user.id}
            createTodo={createTodo}
            handleAddModal={handleAddModal}
          ></AddTodoModal>
        )}
        <View margin="3rem 0">
          <Table caption="" highlightOnHover={false}>
            <TableHead>
              <TableRow>
                <TableCell as="th">No.</TableCell>
                <TableCell as="th">Title</TableCell>
                <TableCell as="th">Status</TableCell>
                <TableCell as="th">Priority</TableCell>
                <TableCell as="th">Milestone</TableCell>
                <TableCell as="th">Discription</TableCell>
                <TableCell as="th"></TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {todos.map((todo, index) => (
                <TableRow key={index}>
                  <TableCell>{index}</TableCell>
                  <TableCell>{todo.title}</TableCell>
                  <TableCell>{todo.status}</TableCell>
                  <TableCell>{todo.priority}</TableCell>
                  <TableCell>
                    {todo.start} - {todo.end}
                  </TableCell>
                  <TableCell>
                    {todo.description ? todo.description : "-"}
                  </TableCell>
                  <TableCell>
                    <Button onClick={() => deleteTodo(todo)}>Delete</Button>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </View>
      </View>
    </LocalizationProvider>
  );
}

export default withAuthenticator(App);
```

以下のimport文でGraphQLのクエリを使えるようにします。
```
import {
  createTodo as createTodoMutation,
  deleteTodo as deleteTodoMutation,
} from "./graphql/mutations";
```

`createTodo`関数は、以下の処理でAPIを利用してToDoタスクの情報をデータベースに追加します。`query`にデータ追加のクエリ、`valiables`に追加するデータを指定します。
```
await API.graphql({
      query: createTodoMutation,
      variables: { input: data },
    });
```

`deleteTodo`関数は、以下の処理でAPIを利用して特定のToDoタスクの情報をデータベースから削除します。`query`にデータ削除のクエリ、`valiables`に削除するデータの`id`を指定します。
```
await API.graphql({
      query: deleteTodoMutation,
      variables: { input: { id } },
    });
```

以下の処理では、ログインしたユーザーの情報を取得しています。
取得したユーザーのデータは`user`オブジェクトに格納しています。`user`のデータは、`AddTodoModal`コンポーネントなどに渡します。
```
useEffect(() => {
    Auth.currentAuthenticatedUser().then((userInfo) => {
      setUser({ id: userInfo.attributes.sub, name: userInfo.username });
    });
  }, []);
```

また、`DatePicker`コンポーネントを利用するために、以下のようにimportして画面表示する部分全体を`LocalizationProvider`コンポーネントで囲みます。
```
import { LocalizationProvider } from "@mui/x-date-pickers";
```

アプリを起動して`Add+`ボタンをクリックすると、以下のようなモーダルが表示され、データをデータベースに追加できます。データを追加すると、Dynamo DBにデータが追加されていることを確認できます。
![スクリーンショット 2023-06-24 233043.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/38a3ad20-d293-2ba1-3753-6fa645f8ecc1.png)


データを追加すると、画面にToDoタスクの一覧が表示されます。各タスクの右に表示される`Delete`ボタンをクリックすると、データをデータベースから削除できます。

### データの編集

データの追加と削除ができるようになったので、次はデータの編集ができるようにします。
ToDoタスクの一覧で各タスクの右の`Edit`ボタンをクリックすると、追加モーダルと同様に編集モーダルが表示され、データを編集できるようにします。

まず、編集モーダル用の`EditTodoModal`コンポーネントを作成します。
`src/components/EditTodoModal.js`を作成し、以下のように編集します。
```js:src/components/EditTodoModal.js
import React, { useState } from "react";
import {
  Button,
  Flex,
  Heading,
  SelectField,
  TextField,
  TextAreaField,
  View,
} from "@aws-amplify/ui-react";
import { DatePicker } from "@mui/x-date-pickers";
import dayjs from "dayjs";

const statusList = ["New", "Open", "On Hold", "Resolved", "Close"];
const priorityList = ["Trivial", "Minor", "Major", "Critical", "Blocker"];

export function EditTodoModal({
  userId,
  preTodo,
  updateTodo,
  handleEditModal,
}) {
  const [todo, setTodo] = useState({ ...preTodo, user: userId });

  return (
    <div className="overlay">
      <div className="content">
        <View className="header">
          <Heading level={2}>Edit Todo</Heading>
        </View>
        <View margin="1rem">
          <Flex direction="column">
            <TextField
              placeholder="Input todo title"
              label="Title"
              value={todo.title}
              onChange={(i) => {
                setTodo({ ...todo, title: i.target.value });
              }}
            />
            <TextAreaField
              label="Discription"
              placeholder="Input todo discription"
              value={todo.description}
              onChange={(i) => {
                setTodo({ ...todo, description: i.target.value });
              }}
            />
            <SelectField
              label="Status"
              value={todo.status}
              onChange={(i) => {
                setTodo({ ...todo, status: i.target.value });
              }}
            >
              {statusList.map((item, i) => (
                <option key={i} value={item}>
                  {item}
                </option>
              ))}
            </SelectField>
            <SelectField
              label="Priority"
              value={todo.priority}
              onChange={(i) => {
                setTodo({ ...todo, priority: i.target.value });
              }}
            >
              {priorityList.map((item, i) => (
                <option key={i} value={item}>
                  {item}
                </option>
              ))}
            </SelectField>
            <Flex
              direction="row"
              justifyContent="space-between"
              alignItems="center"
              margin="1rem 0"
            >
              <DatePicker
                label="Start Date"
                value={dayjs(todo.start)}
                onChange={(i) => {
                  setTodo({ ...todo, start: dayjs(i).format("YYYY-MM-DD") });
                }}
              />
              <span>-</span>
              <DatePicker
                label="End Date"
                value={dayjs(todo.end)}
                onChange={(i) => {
                  setTodo({ ...todo, end: dayjs(i).format("YYYY-MM-DD") });
                }}
              />
            </Flex>
          </Flex>
          <Flex
            direction="row"
            justifyContent="flex-end"
            alignItems="center"
            margin="1rem 0"
          >
            <Button onClick={handleEditModal}>Cansel</Button>
            <Button
              onClick={() => {
                updateTodo(todo);
                handleEditModal();
              }}
            >
              Edit
            </Button>
          </Flex>
        </View>
      </div>
    </div>
  );
}
```

`EditTodoModal`コンポーネントには以下を引数として渡す必要があります。
- userId: ログインしているユーザーの情報
- preTodo: すでに追加されているToDoタスクのデータ
- updateTodo: APIを利用してデータを更新する関数
- handleEditModal: 編集モーダルの表示を切り替える関数

`EditTodoModal`コンポーネントが作成できました。

次にAPIを利用してデータの編集できるように`src/App.js`を以下のように修正します。
```src/App.js
import "./App.css";
import { Amplify, Auth, API } from "aws-amplify";
import config from "./aws-exports";
import "@aws-amplify/ui-react/styles.css";
import {
  withAuthenticator,
  Button,
  Flex,
  Table,
  TableCell,
  TableBody,
  TableHead,
  TableRow,
  Heading,
  View,
} from "@aws-amplify/ui-react";
import { listTodos } from "./graphql/queries";
import {
  createTodo as createTodoMutation,
  updateTodo as updateTodoMutation,
  deleteTodo as deleteTodoMutation,
} from "./graphql/mutations";
import React, { useState, useEffect } from "react";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AddTodoModal } from "./components/AddTodoModal";
import { EditTodoModal } from "./components/EditTodoModal";

Amplify.configure(config);

function App({ signOut }) {
  const [user, setUser] = useState({ id: "", name: "" });
  const [todos, setTodos] = useState([]);
  const [preTodo, setPreTodo] = useState({});
  const [isOpenAddModal, setIsOpenAddModal] = useState(false);
  const [isOpenEditModal, setIsOpenEditModal] = useState(false);

  useEffect(() => {
    Auth.currentAuthenticatedUser().then((userInfo) => {
      setUser({ id: userInfo.attributes.sub, name: userInfo.username });
    });
  }, []);

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

  async function fetchTodos() {
    const apiData = await API.graphql({ query: listTodos });
    const todosFromAPI = apiData.data.listTodos.items;
    setTodos(todosFromAPI);
  }

  function handleAddModal() {
    if (isOpenAddModal === false) {
      setIsOpenAddModal(true);
    } else if (isOpenAddModal === true) {
      setIsOpenAddModal(false);
    }
  }

  function handleEditModal() {
    if (isOpenEditModal === false) {
      setIsOpenEditModal(true);
    } else if (isOpenEditModal === true) {
      setIsOpenEditModal(false);
    }
  }

  async function createTodo(event) {
    const data = {
      title: event.title,
      status: event.status,
      priority: event.priority,
      start: event.start,
      end: event.end,
      description: event.description,
      user: event.user,
    };
    await API.graphql({
      query: createTodoMutation,
      variables: { input: data },
    });
    fetchTodos();
  }

  async function updateTodo(event) {
    const data = {
      id: event.id,
      title: event.title,
      status: event.status,
      priority: event.priority,
      start: event.start,
      end: event.end,
      description: event.description,
      user: event.user,
    };
    await API.graphql({
      query: updateTodoMutation,
      variables: { input: data },
    });
    fetchTodos();
  }

  async function deleteTodo({ id }) {
    const newTodos = todos.filter((todo) => todo.id !== id);
    setTodos(newTodos);
    await API.graphql({
      query: deleteTodoMutation,
      variables: { input: { id } },
    });
  }

  return (
    <LocalizationProvider dateAdapter={AdapterDayjs}>
      <View className="App" margin="0 3rem">
        <View className="header">
          <Flex
            direction="row"
            justifyContent="space-between"
            alignItems="center"
          >
            <Heading level={1}>My Todo App</Heading>
            <Flex direction="row">
              <p>{user.name}</p>
              <Button onClick={signOut}>Sign Out</Button>
            </Flex>
          </Flex>
        </View>
        <View className="add-todo-button" margin="1rem 1rem">
          <Flex
            direction="row"
            justifyContent="space-between"
            alignItems="center"
          >
            <Button onClick={handleAddModal}>Add +</Button>
          </Flex>
        </View>
        {isOpenAddModal === true && (
          <AddTodoModal
            userId={user.id}
            createTodo={createTodo}
            handleAddModal={handleAddModal}
          ></AddTodoModal>
        )}
        {isOpenEditModal === true && (
          <EditTodoModal
            userId={user.id}
            preTodo={preTodo}
            updateTodo={updateTodo}
            handleEditModal={handleEditModal}
          ></EditTodoModal>
        )}
        <View margin="3rem 0">
          <Table caption="" highlightOnHover={false}>
            <TableHead>
              <TableRow>
                <TableCell as="th">No.</TableCell>
                <TableCell as="th">Title</TableCell>
                <TableCell as="th">Status</TableCell>
                <TableCell as="th">Priority</TableCell>
                <TableCell as="th">Milestone</TableCell>
                <TableCell as="th">Discription</TableCell>
                <TableCell as="th"></TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {todos.map((todo, index) => (
                <TableRow key={index}>
                  <TableCell>{index}</TableCell>
                  <TableCell>{todo.title}</TableCell>
                  <TableCell>{todo.status}</TableCell>
                  <TableCell>{todo.priority}</TableCell>
                  <TableCell>
                    {todo.start} - {todo.end}
                  </TableCell>
                  <TableCell>
                    {todo.description ? todo.description : "-"}
                  </TableCell>
                  <TableCell>
                    <Button
                      margin="0 1rem"
                      onClick={() => {
                        setPreTodo(todo);
                        handleEditModal();
                      }}
                    >
                      Edit
                    </Button>
                    <Button onClick={() => deleteTodo(todo)}>Delete</Button>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </View>
      </View>
    </LocalizationProvider>
  );
}

export default withAuthenticator(App);
```

以下のようにimport文を追記してGraphQL更新のクエリを使えるようにします。
```
import {
  createTodo as createTodoMutation,
  updateTodo as updateTodoMutation,
  deleteTodo as deleteTodoMutation,
} from "./graphql/mutations";
```

`updateTodo`関数は、以下の処理でAPIを利用してデータベースのToDoタスクの情報を更新します。`query`にデータ編集のクエリ、`valiables`に編集するデータを指定します。
```
await API.graphql({
      query: createTodoMutation,
      variables: { input: data },
    });
```

これでアプリ画面の作成も完了です。
アプリを起動して、タスク一覧の`Edit`ボタンをクリックすると、編集モーダルで各toDoタスクのデータを更新できます。

## デプロイ

アプリの作成が完了したので、最後に`master`ブランチにプッシュしてアプリをデプロイしましょう。

これで、認証機能を備えたToDoタスクの管理アプリを公開することができました。

# まとめ

AWS Amplifyを利用して簡単なToDoアプリを開発して、公開までしてみました。
データベースの利用やデプロイなどバックエンドのことはほとんど意識せずに、アプリ開発ができました。実際にこの記事の中でもでも実装についての内容が多く、バックエンドの内容は少なくなってしまいました。
Amplifyの中にUIコンポーネントも用意されているので、フロントの実装もやりやすかったです。

VueやAngularなどにも対応しているので、すでにReact以外で開発したアプリでも簡単に様々なAWSサービスと連携させることができそうです。他にもいろいろと設定をいじったり、ToDoアプリもいろいろ機能を搭載できそうなので、今後も開発を進めていきたいと思います。