AWS Amplify, GraphQL, Amplify AuthでTODOアプリ
概要
AWS Amplify, GraphQL, Amplify AuthでTODOアプリを作成しました。
AWSアカウント作成とAmplifyアプリケーションの作成
以下を参考にAWSアカウントを作成します。画面の項目を順に入力していきます。
アカウント作成後にConsoleから新規Amplifyアプリケーションを作成します。
Amplify Hosting environmentとGitHubのリポジトリを紐づけて、ホスティングする。
GithubでAmplifyと紐づけるリポジトリを作成します。
フロントエンドはReactを使うので、以下公式を参考にローカルで作成していきます。
Reactのドキュメントではフレームワークを使用することが推奨されているので、Next.jsで作成します。
npx create-next-app@latest
ローカルでNext.jsのプロジェクトを作成出来たら、GitHubで新規リポジトリを作成し、ローカルで作成したNext.jsのプロジェクトをpushします。
GitHubのリポジトリと、新規Amplifyアプリケーションを作成出来たら、それらを紐づけてホスティングの設定をしていきます。
AmplifyのConsoleからGitHubを選択し、先ほど作成したリポジトリを選択します。
その後バックエンド環境やロールを選択していきます。
ホスティングの設定が完了すると、ビルドとデプロイ処理が走って数分で完了します。
Amplify ConsoleからURLが確認でき、無事ビルド出来ていればホスティング完了です。
Amplify CLIのインストール
続いてAmplify CLIをインストールしていきます。
Amplify CLIはAmplifyのバックエンド環境を作成、管理出来るコマンドラインツールです。
以下公式ドキュメントを参考にインストールしていきます。
以下コマンドでインストールします。
npm install -g @aws-amplify/cli
念の為インストールされていることを確認します。
amplify --version
インストールが完了したら次は設定をしていきます。
amplify configure
- ブラウザが立ち上がりAWS Consoleへのログインが求められます。既にログインしている場合はターミナルに戻ります。
- リージョンを選択します。
- IAMユーザーの作成をします。ブラウザが立ち上がるのでユーザー名を入力します。
- 許可のオプションを設定します。
ポリシーを直接アタッチする
を選択し、許可ポリシーからAdministratorAccess-Amplify
を選択、ユーザーを作成します。
- IAMユーザーが作成されたので、アクセスキーを作成していきます。ユースケースは
コマンドラインインターフェイス
を選択。
- アクセスキーが作成されたので、アクセスキー、シークレットアクセスキーをそれぞれコピーして、ターミナルに戻りそれぞれペーストします。
Enter the access key of the newly created user:
? accessKeyId: ********************
? secretAccessKey: ****************************************
これでAmplify CLIのインストールと設定が完了です。
ローカルでAmplifyのバックエンド環境を接続する
ローカルからAmplifyのバックエンド環境に接続していきます。
AmplifyのConsoleからBackend environmentのタブを選択します。
バックエンド環境とローカルのプロジェクトを接続するコマンドがあるので、それをコピーしてプロジェクトのルートディレクトリで実行します。
その後ターミナルで対話形式でエディタやフレームワークを選択していきます。
全て完了するとローカルにamplify
というディレクトリが作成されるので、これで接続完了です。
最後にamplifyのライブラリをインストールします。
npm install aws-amplify @aws-amplify/ui-react
ログイン画面の作成
今回はログイン画面を作成します。
以下を参考にAmplify Authを追加します。
ターミナルでコマンドをAuthを追加するコマンドを実行
amplify add auth
対話形式で設定と、サインインの形式を対話形式で聞かれるので回答していきます。
そうするとamplify/backend
配下にauth
ディレクトリが作成されるので
amplify push
でデプロイします。デプロイには数分かかります。
これでAmplifyのバックエンド環境にAuthが追加されました。
次にログイン画面のUIを作成します。
まずはフロントエンドでAmplifyの機能が使えるように以下のコードを追加します。
import { Amplify } from "aws-amplify";
import awsconfig from "../aws-exports";
Amplify.configure(awsconfig);
UIはAmplifyUIのAuthenticatorコンポーネントを使用します。
"use client";
import { Amplify } from "aws-amplify";
import awsconfig from "../aws-exports";
Amplify.configure(awsconfig);
import "@aws-amplify/ui-react/styles.css";
import { Authenticator } from "@aws-amplify/ui-react";
import styles from "./page.module.css";
export default function Home() {
return (
<main className={styles.main}>
<Authenticator>
{({ signOut }) => (
<main>
<h1>ログイン成功です</h1>
<button onClick={signOut}>Sign out</button>
</main>
)}
</Authenticator>
</main>
);
}
これでログイン画面が作成出来ます。
yarn run dev
でローカル環境を立ち上げると、以下のような画面になっています。
CreateAccountタブからアカウントを作成して、SignInします。
SignInに成功すると
Authenticator内のHTMLが画面に表示されるようになりました。
これでログイン画面の作成が完了しました。
API作成
ドキュメントを参考にAPIを作成します。
今回はQraphQLでAPIを作成します。
ターミナルで以下コマンドを入力します。
amplify add api
対話形式でQraphQLを選択します。
そうするとローカルでschema.graphqlが作成されます。
type Todo @model {
id: ID!
name: String!
description: String
}
既にTodoのmodelが作成されているので、これを使用していきます。
amplify push
でAmplifyのバックエンド環境に反映しましょう。
これでGraphQLのAPIが作成完了です。
Chakra UI
画面を作るにあたってChakraUIを使っていきます。
ライブラリをインストールしていきます。
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion @chakra-ui/next-js
プロジェクトでChakraUIを使えるように、Provider.tsxを作成します。
"use client";
import { CacheProvider } from "@chakra-ui/next-js";
import { ChakraProvider } from "@chakra-ui/react";
export function Provider({ children }: { children: React.ReactNode }) {
return (
<CacheProvider>
<ChakraProvider>{children}</ChakraProvider>
</CacheProvider>
);
}
Next.jsのAppRouterを使っているので、ChakraProviderだけでなくCacheProviderも使用します。
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className={inter.className}>
<Provider>{children}</Provider>
</body>
</html>
);
}
Layout.tsxで読み込むことで、ChakraUIが使用できるようになります。
UIの作成
TODOアプリのUIを作成します。
まずはTODOのリストコンポーネントを作成します。
"use client";
import { UnorderedList, ListItem } from "@chakra-ui/react";
import { ListType } from "../types";
type ListProps = {
list: ListType;
onOpenModal: (id: string) => void;
};
export const List = ({ list, onOpenModal }: ListProps) => {
return (
<UnorderedList>
{list.map((item) => (
<ListItem key={item.id}>
<button onClick={() => onOpenModal(item.id)}>{item.name}</button>
</ListItem>
))}
</UnorderedList>
);
};
TODOをクリックすると、編集モーダルを開く想定で、propsでonOpenModal関数を受け取れるようにしています。
次にTODO追加とTODO編集のコンポーネントを作成します。
"use client";
import {
Button,
Stack,
FormControl,
FormLabel,
Input,
Textarea,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
} from "@chakra-ui/react";
import {
UseFormHandleSubmit,
SubmitHandler,
UseFormRegister,
} from "react-hook-form";
import { FormType } from "../types";
type AddModalProps = {
isOpen: boolean;
onClose: () => void;
handleSubmit: UseFormHandleSubmit<FormType>;
onSubmit: SubmitHandler<FormType>;
register: UseFormRegister<FormType>;
};
export const AddModal = ({
isOpen,
onClose,
handleSubmit,
onSubmit,
register,
}: AddModalProps) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>TODO追加</ModalHeader>
<ModalCloseButton />
<ModalBody>
<form id="add" onSubmit={handleSubmit(onSubmit)}>
<Stack>
<FormControl>
<FormLabel>タイトル</FormLabel>
<Input {...register("name", { required: true })} />
</FormControl>
<FormControl>
<FormLabel>説明文</FormLabel>
<Textarea {...register("description", { required: true })} />
</FormControl>
</Stack>
</form>
</ModalBody>
<ModalFooter>
<Button type="submit" form="add">
追加
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
"use client";
import {
Button,
Stack,
FormControl,
FormLabel,
Input,
Textarea,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
} from "@chakra-ui/react";
import {
UseFormHandleSubmit,
SubmitHandler,
UseFormRegister,
} from "react-hook-form";
import { FormType } from "../types";
type EditModalProps = {
isOpen: boolean;
onClose: () => void;
handleSubmit: UseFormHandleSubmit<FormType>;
onSubmit: SubmitHandler<FormType>;
register: UseFormRegister<FormType>;
onDeleteItem: () => void;
};
export const EditModal = ({
isOpen,
onClose,
handleSubmit,
onSubmit,
register,
onDeleteItem,
}: EditModalProps) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>TODO編集</ModalHeader>
<ModalCloseButton />
<ModalBody>
<form id="edit" onSubmit={handleSubmit(onSubmit)}>
<Stack>
<FormControl>
<FormLabel>タイトル</FormLabel>
<Input {...register("name", { required: true })} />
</FormControl>
<FormControl>
<FormLabel>説明文</FormLabel>
<Textarea {...register("description", { required: true })} />
</FormControl>
</Stack>
</form>
</ModalBody>
<ModalFooter>
<Stack direction={"row"}>
<Button type="button" onClick={onDeleteItem}>
削除
</Button>
<Button type="submit" form="edit">
保存
</Button>
</Stack>
</ModalFooter>
</ModalContent>
</Modal>
);
};
Chakra UIのModalコンポーネントを使っています。
入力フォームではReact Hook Formを使う想定で、propsで受け取れるようにしています。
次に処理をまとめたカスタムフックを作成します。
import { useState } from "react";
import { useDisclosure } from "@chakra-ui/react";
import { useForm, SubmitHandler } from "react-hook-form";
import { ListType, FormType } from "../types";
export const useTodo = () => {
// モーダルの開閉
const {
isOpen: isOpenAddModal,
onOpen: onOpenAddModal,
onClose: onCloseAddModal,
} = useDisclosure();
const {
isOpen: isOpenEditModal,
onOpen: onOpenEditModal,
onClose: onCloseEditModal,
} = useDisclosure();
// TODOリストの状態管理
const [list, setList] = useState<ListType>();
// 編集するTODOのIDの状態管理
const [currentId, setCurrentId] = useState<string>();
// React Hook Formの呼び出し
const {
register: addRegister,
handleSubmit: handleSubmitAdd,
setValue: setValueAdd,
} = useForm<FormType>();
const {
register: editRegister,
handleSubmit: handleSubmitEdit,
setValue: setValueEdit,
} = useForm<FormType>();
// 編集モーダルを開く際の処理
const onOpenAndSetValueEditModal = (id: string) => {
if (!list) return;
const currentItem = list.find((item) => item.id === id);
setValueEdit("name", currentItem ? currentItem.name : "");
setValueEdit("description", currentItem ? currentItem.description : "");
setCurrentId(id);
onOpenEditModal();
};
// TODO追加処理
const onSubmitAdd: SubmitHandler<FormType> = (data) => {
const newItem = {
id: (Math.floor(Math.random() * 1000000000) + 1).toString(),
name: data.name,
description: data.description,
};
setList((prev) => {
if (prev) {
return [...prev, newItem];
} else {
return [newItem];
}
});
setValueAdd("name", "");
setValueAdd("description", "");
onCloseAddModal();
};
// TODO更新処理
const onSubmitEdit: SubmitHandler<FormType> = (data) => {
if (!list) return;
const newList = list.map((item) =>
item.id === currentId
? {
id: item.id,
name: data.name,
description: data.description,
}
: item
);
setList(newList);
onCloseEditModal();
};
// TODOの削除処理
const onDeleteItem = () => {
if (!list) return;
const newList = list.filter((item) => item.id !== currentId);
setList(newList);
onCloseEditModal();
};
return {
isOpenAddModal,
onOpenAddModal,
onCloseAddModal,
isOpenEditModal,
onCloseEditModal,
list,
addRegister,
handleSubmitAdd,
editRegister,
handleSubmitEdit,
onOpenAndSetValueEditModal,
onSubmitAdd,
onSubmitEdit,
onDeleteItem,
};
};
最後に作成した各コンポーネントと、カスタムフックを使って画面を作成します。
"use client";
import { Amplify } from "aws-amplify";
import awsconfig from "../aws-exports";
Amplify.configure(awsconfig);
import "@aws-amplify/ui-react/styles.css";
import { Authenticator } from "@aws-amplify/ui-react";
import { Button, Heading, Stack } from "@chakra-ui/react";
import styles from "./page.module.css";
import { List, AddModal, EditModal } from "./components";
import { useTodo } from "./hooks/useTodo";
export default function Home() {
const {
isOpenAddModal,
onOpenAddModal,
onCloseAddModal,
isOpenEditModal,
onCloseEditModal,
list,
addRegister,
handleSubmitAdd,
editRegister,
handleSubmitEdit,
onOpenAndSetValueEditModal,
onSubmitAdd,
onSubmitEdit,
onDeleteItem,
} = useTodo();
return (
<main className={styles.main}>
<Authenticator>
{({ signOut }) => (
<>
<Heading>TODOアプリ</Heading>
<Stack
minWidth={"800px"}
alignItems={"flex-start"}
mt={"32px"}
spacing={"16px"}
>
<Button onClick={onOpenAddModal}>追加</Button>
{list && (
<List list={list} onOpenModal={onOpenAndSetValueEditModal} />
)}
<Button onClick={signOut}>サインアウト</Button>
</Stack>
<AddModal
isOpen={isOpenAddModal}
onClose={onCloseAddModal}
handleSubmit={handleSubmitAdd}
onSubmit={onSubmitAdd}
register={addRegister}
/>
<EditModal
isOpen={isOpenEditModal}
onClose={onCloseEditModal}
handleSubmit={handleSubmitEdit}
onSubmit={onSubmitEdit}
register={editRegister}
onDeleteItem={onDeleteItem}
/>
</>
)}
</Authenticator>
</main>
);
}
これでTODOアプリのUI作成が完了しました。
ここまででTODOの追加、編集、削除の動作を作成出来たので、最後にAmplifyのAPIと連携していきます。
GraphQL APIとの連携
先ほど作成したGraphQL APIを用いて、実際に入力した項目でTODOを作成、編集、削除出来るようにしていきます。
こちらのドキュメントを参考にします。
まずはuseTodo.ts
で必要なライブラリをimportします。
import { API, graphqlOperation } from "aws-amplify";
import { GraphQLQuery } from "@aws-amplify/api";
API作成時にAmplifyで自動生成されたquery, mutation, typeもimportします。
import { listTodos } from "@/graphql/queries";
import { createTodo, updateTodo, deleteTodo } from "@/graphql/mutations";
import {
ListTodosQuery,
CreateTodoMutation,
UpdateTodoMutation,
DeleteTodoMutation,
} from "@/API";
これらはAPI作成時にschema.graphql
で定義した@modelから自動生成されています。
TODOを追加処理の関数を編集します。
const onSubmitAdd: SubmitHandler<FormType> = async (data) => {
// 入力された項目で、TODOリストを作成
const responce = await API.graphql<GraphQLQuery<CreateTodoMutation>>(
graphqlOperation(createTodo, {
input: { name: data.name, description: data.description },
})
);
// レスポンスのデータを整形
const createdItem = responce.data?.createTodo;
if (!createdItem) return;
const newItem = {
id: createdItem.id,
name: createdItem.name,
description: createdItem.description || "",
};
// UIを更新する
setList((prev) => {
if (prev) {
return [...prev, newItem];
} else {
return [newItem];
}
});
// フォームの値を空に戻してモーダルを閉じる
setValueAdd("name", "");
setValueAdd("description", "");
onCloseAddModal();
};
API.graphql
メソッドの引数にgraphqlOperation
を指定します。
graphqlOperation
の第一引数には、自動生成されたcreateTodo
というmutationを渡して、第二引数に入力された値を渡すことで、新規で新しくTODOを追加できます。
responseから表示用にデータを整形してから、TODOリストのstateも更新して、UIにリアルタイムで反映できるようにします。
これでTODOを新規作成できるようになったので、作成したTODOをAPIから取得できるようにします。
// TODOリストをQueryで取得してセットする。
useEffect(() => {
const queryTodos = async () => {
const todoData = await API.graphql<GraphQLQuery<ListTodosQuery>>(
graphqlOperation(listTodos)
);
const todoList = todoData.data?.listTodos?.items;
if (!todoList) return;
// データの整形
const transformedTodos = todoList.map((item) => ({
id: item?.id || "",
name: item?.name || "",
description: item?.description || "",
}));
setList(transformedTodos);
};
queryTodos();
}, []);
新規作成と同様にAPI.graphql
メソッドの引数にgraphqlOperation
を指定します。
データを取得するので、graphqlOperation
の引数に、自動生成されたlistTodos
queryを渡します。
取得したデータを整形して、listのstateを更新します。
これで作成したTODOが画面に表示されるようになりました。
最後にTODOの更新と削除もできるようにします。
// TODO更新処理
const onSubmitEdit: SubmitHandler<FormType> = async (data) => {
if (!list) return;
// 現在編集しているTODOのIDと、入力された項目で更新する
const responce = await API.graphql<GraphQLQuery<UpdateTodoMutation>>(
graphqlOperation(updateTodo, {
input: {
id: currentId,
name: data.name,
description: data.description,
},
})
);
// レスポンスのデータを整形
const updatedItem = responce.data?.updateTodo;
if (!updatedItem) return;
// UIを更新してモーダルを閉じる
const newList = list.map((item) =>
item.id === currentId
? {
id: updatedItem.id,
name: updatedItem.name,
description: updatedItem.description || "",
}
: item
);
setList(newList);
onCloseEditModal();
};
// TODOの削除処理
const onDeleteItem = async () => {
if (!list) return;
// 現在編集しているTODOのIDを渡して削除する
const responce = await API.graphql<GraphQLQuery<DeleteTodoMutation>>(
graphqlOperation(deleteTodo, { input: { id: currentId } })
);
// UIを更新してモーダルを閉じる
const deletedId = responce.data?.deleteTodo?.id;
if (!deletedId) return;
const newList = list.filter((item) => item.id !== deletedId);
setList(newList);
onCloseEditModal();
};
更新と削除も、新規追加時と同様にAPI.graphql
とgraphqlOperation
を使用します。
それぞれ更新はupdateTodo
、削除はdeleteTodo
mutationを渡します。
第二引数にはそれぞれ必要な値を渡して、レスポンスからlistのstateを更新することで、UIもリアルタイムで更新します。
これでAmplifyを使ったTODOアプリの完成です!
Discussion