4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

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を作っていきます。

  • 公式サイト

料金

以下の制限で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

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

今回はGitHubのリポジトリからAWS Amplifyにコードを接続するため、GitHubのリポジトリに作成したアプリをプッシュする必要があります。
GitHubにリポジトリを作成します。
Repository Nameなどを設定して、Create repositoryボタンをクリックします。
スクリーンショット 2023-06-11 222054.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マネジメントコンソールにログインし、AWS Amplifyにアクセスします。
まだAWS Amplifyを使用したことがない場合は、以下のような画面に遷移しますので、ウェブアプリケーションをホスト使用を開始するをクリックします。
スクリーンショット 2023-06-11 224557.png

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

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

認証に成功すると以下の画面に遷移します。最近更新されたリポジトリで作成したリポジトリを選択し、ブランチを選択したら次へをクリックします。スクリーンショット 2023-06-11 225246.png

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

以下の画面のステータスバーでデプロイが成功していれば、AWS Amplifyでのアプリのデプロイが完了です。
赤線で囲んだURLにアクセスすると、作成したアプリの画面が開きます。
スクリーンショット 2023-06-11 231055.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

Studioを起動するをクリックすると、バックエンドを管理するためのビジュアルインターフェイスが別タブで開きます。
スクリーンショット 2023-06-19 001045.png

Backend environmentsタブに戻り、ローカル設定手順をクリックします。
表示されるコマンドをターミナルで実行します。
スクリーンショット 2023-06-19 001712.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

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

Amplify Studioを起動して、再度メニューUser managementをクリックすると、登録されたユーザーの情報を確認したり、ユーザーグループを設定したりできます。
Authenticationメニューからは、パスワードポリシーの設定など認証機能の設定を行うことができます。
スクリーンショット 2023-06-19 215335.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をエディターで開き、以下を追記して保存します。
ちなみにデータのスカラー型はこちらを参照ください。

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 を作成する
  2. DynamoDB テーブルを作成する
  3. API のクエリに使用できるローカルGraphQLオペレーションをsrc/graphqlにあるフォルダーに作成する
$ amplify push --y

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

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

データの表示

APIを利用して取得したデータを表示するようなアプリ画面を作成していきます。
利用するコンポーネントは認証で利用したwithAuthenticatorも含めて、以下を利用します
Amplify UI Componentsの詳細はこちらを参照ください。

  • withAuthenticator,
  • Button
  • Flex
  • Table
  • TableCell
  • TableBody
  • TableHead
  • TableRow
  • Heading
  • View

では、画面表示のコードを修正していきます。
src/App.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

データの追加・削除

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

モーダル表示のためsrc/index.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を以下のように修正します。

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

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

データの編集

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

まず、編集モーダル用のEditTodoModalコンポーネントを作成します。
src/components/EditTodoModal.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アプリもいろいろ機能を搭載できそうなので、今後も開発を進めていきたいと思います。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?