Amazon Web Services ブログ
Next.js と Amplify を活用した製品ロードマップアプリの構築
本記事では、私たちがロードマップを公開している自動車会社であると想像してみましょう。私たちには世界中のユーザーがいて、車載エンターテイメントシステムにどのような機能が提供されたかを定期的にチェックしています。
ここでは、プロダクトマネージャーがログインしてロードマップを更新し、ロードマップページに反映させるための管理ページを構築します。 ロードマップページは世界中の多くの読者が閲覧し、更新頻度が低いため、Incremental Static Regeneration (ISR) を用いた Static Site Generator (SSG) の最適な候補になります。
本記事では、「Deploy a Next.js 13 app with authentication to AWS Amplify」を基に開発し、プロジェクトの初期化、Amazon Cognito 認証の実装、Amplify Hosting へのデプロイを行います。
GraphQL API のデプロイ
プロジェクトのルートディレクトリで以下のコマンドを実行し、データを保存する GraphQL API を追加します。
amplify add api
Amplify CLI のプロンプトには以下のように答えます。
? Select from one of the below mentioned services: (Use arrow keys)
❯ GraphQL
REST
? Here is the GraphQL API that we will create. Select a setting to edit or continue (Use arrow keys)
Name: testamplify
Authorization modes: API key (default, expiration time: 7 days from now)
Conflict detection (required for DataStore): Disabled
❯ Continue
? Choose a schema template:
Single object with fields (e.g., “Todo” with ID, name, description)
One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”)
❯ Blank Schema
? Do you want to edit the schema now? (Y/n) › Y
次に、schema.graphql
の内容を以下のように書き換えます。
type Feature @model @auth(rules: [{ allow: owner }, { allow: public, operations: [read] }]) {
id: ID!
title: String!
released: Boolean!
description: String
internalDoc: String
}
最後に、以下のコマンドを実行し、API をデプロイします。
amplify push
次に、プロダクトマネージャがロードマップを作成、更新、削除するための管理画面を作成します。
pages/admin.js
ファイルを作成し、以下のコードを追加して、このシリーズの最初の投稿で行ったように、Amplify の withAuthenticator
コンポーネントを使用し、Cognito で認証を行うページを作成します。
// pages/admin.js
import {
Button,
Divider,
Flex,
Heading,
View,
withAuthenticator,
} from "@aws-amplify/ui-react";
import { Auth } from "aws-amplify";
import Link from "next/link";
import React, { useState } from "react";
function Admin() {
// define state to be used later
const [activeFeature, setActiveFeature] = useState(undefined);
return (
<View padding="2rem">
<Flex justifyContent={"space-between"}>
<Link href={"/admin"}>
<Heading level={2}>AmpliCar Roadmap Admin</Heading>
</Link>
<Flex alignItems={"center"}>
<Button type="button" onClick={() => Auth.signOut()}>
Sign out
</Button>
</Flex>
</Flex>
<Divider marginTop={"medium"} marginBottom={"xxl"} />
<Flex></Flex>
</View>
);
}
export default withAuthenticator(Admin);
ここでは、管理ページ用の React コンポーネント Admin
を作成します。aws-amplify/ui-react
パッケージの withAuthenticator
コンポーネントを使用して、認証機能をコンポーネントに追加します。
また、このコンポーネントは Amplify UI の Button
、Divider
、Flex
、Heading
、View
コンポーネントをインポートして使用し、サインアウトボタン、ディバイダー、後で使用するレイアウト要素を含むトップナビゲーションバーをレンダリングします。 サインアウトボタンがクリックされると、Auth.signOut()
メソッドが呼び出され、ユーザーがサインアウトします。
最後に、状態を管理するための React の useState
フックを使って、activeFeature
と setActiveFeature
で状態を追加します。
ブラウザで http://localhost:3000/admin
にアクセスすると以下のような画面が表示されます。
アカウントを作成してサインインすると、ヘッダーにサインアウトボタンが表示されます。
ロードマップの機能を作成・更新するためのフォームを作成します。 プロジェクトのルートディレクトリに components
ディレクトリを作成し、FeatureForm.js
作成し、以下のコードに置き換えます。
// components/FeatureForm.js
import {
Button,
Flex,
Heading,
SwitchField,
Text,
TextField,
View,
} from "@aws-amplify/ui-react";
import { API } from "aws-amplify";
import React, { useEffect, useState } from "react";
import { createFeature, updateFeature } from "../src/graphql/mutations";
function FeatureForm({ feature = null, setActiveFeature }) {
const [id, setId] = useState(undefined);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [isReleased, setReleased] = useState(false);
useEffect(() => {
if (feature) {
setId(feature.id);
setTitle(feature.title);
setDescription(feature.description);
setReleased(feature.released);
}
}, [feature]);
function resetFormFields() {
setId(undefined);
setTitle("");
setDescription("");
setReleased(false);
}
async function handleSaveFeature() {
try {
await API.graphql({
authMode: "AMAZON_COGNITO_USER_POOLS",
query: feature ? updateFeature : createFeature,
variables: {
input: {
id: feature ? id : undefined,
title,
description,
released: isReleased
},
},
});
feature && setActiveFeature(undefined);
resetFormFields();
} catch ({ errors }) {
console.error(...errors);
throw new Error(errors[0].message);
}
}
return (
<View>
<Heading marginBottom="medium" level={5}>
{feature ? "Edit" : "New"} Feature
</Heading>
<Flex direction={"column"} basis={"max-content"}>
<TextField
value={title}
label="Title"
errorMessage="There is an error"
name="title"
onChange={(e) => setTitle(e.target.value)}
/>
<TextField
value={description}
name="description"
label="Description"
errorMessage="There is an error"
onChange={(e) => setDescription(e.target.value)}
/>
<SwitchField
isChecked={isReleased}
isDisabled={false}
label="Released?"
labelPosition="start"
onChange={() => setReleased(!isReleased)}
/>
<Flex marginTop="large">
<Button
onClick={() => {
setActiveFeature(undefined);
resetFormFields();
}}
>
Cancel
</Button>
<Button onClick={() => handleSaveFeature()}>Save</Button>
</Flex>
</Flex>
</View>
);
}
export default FeatureForm;
このコードでは、FeatureForm
という React コンポーネントを定義します。FeatureForm
コンポーネントは、feature
props と setActiveFeature
props を受け取ります。これらは、フォームフィールドにデータを入力し、保存された後にフォームをリセットするために使用されます。コンポーネントは useState
フックを使用して、機能のタイトル、説明、リリース済みかどうかを含むフォームフィールドの状態を追跡します。
コンポーネントがレンダリングされると、feature
props が渡されたかどうかをチェックし、渡された場合はuseEffect
フックを使ってフォームフィールドに機能を入力します。そうでなければ、フォームフィールドは空のままです。
このコンポーネントには、機能のタイトル、説明、リリースステータスを入力するためのいくつかのフォームと、保存ボタンとキャンセルボタンがあります。save
ボタンがクリックされると、コンポーネントは API.graphql
を介して createFeature
mutation を発行し、その機能を GraphQL API を使用して保存します。コンポーネントが feature props でレンダリングされた場合、updateFeature
mutation を使用してデータベース内の既存の機能を更新します。
機能が保存された後、コンポーネントはフォームフィールドをリセットし、オプションで setActiveFeature
コールバック props を呼び出して Admin
コンポーネントの activeFeature
をリセットします。
pages/admin
の Admin
コンポーネントを更新して FeatureForm
コンポーネントを追加し、ロードマップ管理画面に表示されるようにします。また、機能を編集しているかどうかを追跡するステート変数を追加します。
// pages/admin.js
import {
Button,
Divider,
Flex,
Heading,
View,
withAuthenticator,
} from "@aws-amplify/ui-react";
import { Auth } from "aws-amplify";
import Link from "next/link";
import React, { useState } from "react";
import FeatureForm from "../components/FeatureForm";
function Admin() {
const [activeFeature, setActiveFeature] = useState(undefined);
return (
<View padding="2rem">
// ...
<Divider marginTop={"medium"} marginBottom={"xxl"} />
<Flex>
<FeatureForm feature={activeFeature} setActiveFeature**={setActiveFeature} />
</Flex>
</View>
);
}
export default withAuthenticator(Admin);
ページを更新すると、新機能フォームが表示されます。
次に、機能の一覧を表示し、編集や削除を可能にするテーブルを作成しましょう。
components
ディレクトリに新しく FeaturesTable
コンポーネントを作成し、以下のコードを貼り付けます。
// components/FeaturesTable.js
import {
Button,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
View,
} from "@aws-amplify/ui-react";
import { API, graphqlOperation } from "aws-amplify";
import React, { useEffect, useState } from "react";
import { deleteFeature } from "../src/graphql/mutations";
import { listFeatures } from "../src/graphql/queries";
function FeaturesTable({ initialFeatures = [], setActiveFeature }) {
const [features, setFeatures] = useState(initialFeatures);
useEffect(() => {
const fetchFeatures = async () => {
const result = await API.graphql(graphqlOperation(listFeatures));
setFeatures(result.data.listFeatures.items);
};
fetchFeatures();
}, []);
async function handleDeleteFeature(id) {
try {
await API.graphql({
authMode: "AMAZON_COGNITO_USER_POOLS",
query: deleteFeature,
variables: {
input: {
id,
},
},
});
} catch ({ errors }) {
console.error(...errors);
}
}
if (features.length === 0) {
return <View>No features</View>;
}
return (
<Table>
<TableHead>
<TableRow>
<TableCell as="th">Feature</TableCell>
<TableCell as="th">Released</TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{features.map((feature) => (
<TableRow key={feature.id}>
<TableCell>{feature.title}</TableCell>
<TableCell>{feature.released ? "Yes" : "No"}</TableCell>
<TableCell>
<Button size="small" onClick={() => setActiveFeature(feature)}>
Edit
</Button>
<Button
size="small"
onClick={() => handleDeleteFeature(feature.id)}
>
Delete
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
export default FeaturesTable;
このコードでは、FeaturesTable
という React コンポーネントを定義し、機能のテーブルをレンダリングします。テーブルには、各機能のタイトルとリリース済みかどうかが表示され、各機能を編集および削除するためのボタンが用意されています。 このテーブルは、Amplify UI の Table
、TableBody
、TableCell
、TableRow
コンポーネントを使用してレンダリングされます。
FeaturesTable
コンポーネントは、オプションで initialFeatures
の配列と setActiveFeature
関数を props として受け取ります。useState
フックを使用して features
ステート変数に features
リストを格納し、useEffect
フックを使用してコンポーネントのマウント時に GraphQL API から features
リストを取得します。
handleDeleteFeature
関数は、deleteFeature
mutation を呼び出して機能を削除するために使用します。handleDeleteFeature
関数は、テーブル内の各機能の削除ボタンに props として渡され、ボタンがクリックされると、対応する機能が削除されます。
編集ボタンがクリックされると、クリックされた機能を引数として setActiveFeature
関数が呼び出されます。これにより、Admin
コンポーネントの activeFeature
ステート変数が更新され、FeatureForm
コンポーネントが再レンダリングされます。これにより、ユーザーはフォームを使って選択した機能を編集できるようになります。
次に、Pages/admin.js
を更新して、FeaturesTable
コンポーネントを追加します。
// pages/admin.js
import {
// ...
withAuthenticator,
} from "@aws-amplify/ui-react";
import { Auth } from "aws-amplify";
import Link from "next/link";
import React, { useState } from "react";
import FeatureForm from "../components/FeatureForm";
import FeaturesTable from "../components/FeaturesTable";
function Admin() {
const [activeFeature, setActiveFeature] = useState(undefined);
return (
<View padding="2rem">
// ...
<Divider marginTop={"medium"} marginBottom={"xxl"} />
<Flex>
<FeatureForm
feature={activeFeature}
setActiveFeature={setActiveFeature}
/>
<FeaturesTable setActiveFeature={setActiveFeature} />
</Flex>
</View>
);
}
export default withAuthenticator(Admin);
このコンポーネントを追加してリフレッシュすると、まだ機能を追加していないため、No features と表示されるはずです。
Server-Side Rendering(SSR)によるユーザー体験の向上
優れたユーザー体験を提供するために、Server-Side Rendering (SSR) を使って機能を取得するように管理ページを更新しましょう。SSR を使うことで、データなしでページをレンダリングし、別のネットワークリクエストが完了し、データが入力されるのを待つよりも優れたユーザー体験を提供することが出来ます。
// pages/admin.js
import {
Button,
Divider,
Flex,
Heading,
View,
withAuthenticator,
} from "@aws-amplify/ui-react";
import { Auth, withSSRContext } from "aws-amplify";
import Link from "next/link";
import React, { useState } from "react";
import FeatureForm from "../components/FeatureForm";
import FeaturesTable from "../components/FeaturesTable";
import { listFeatures } from "../src/graphql/queries";
export async function getServerSideProps({ req }) {
const SSR = withSSRContext({ req });
const response = await SSR.API.graphql({ query: listFeatures });
return {
props: {
initialFeatures: response.data.listFeatures.items,
},
};
}
function Admin({ initialFeatures }) {
const [activeFeature, setActiveFeature] = useState(undefined);
return (
<View padding="2rem">
// ...
<Divider marginTop={"medium"} marginBottom={"xxl"} />
<Flex>
<FeatureForm
feature={activeFeature}
setActiveFeature={setActiveFeature}
/>
<FeaturesTable
initialFeatures={initialFeatures}
setActiveFeature={setActiveFeature}
/>
</Flex>
</View>
);
}
export default withAuthenticator(Admin);
このコードでは、getServerSideProps
を使用して、listFeatures
query で Amplify API.graphql
を呼び出し、サーバー上の React コンポーネントに注入される props オブジェクトで初期の機能を返します。 Admin
コンポーネントは initialFeatures
を受け取り、FeaturesTable
コンポーネントに渡します。
新機能フォームで、機能を追加してページをリフレッシュすると、右側のテーブルに表示されます。
リアルタイム更新によるユーザー体験の向上
これまで構築したものは動作しますが、プロダクトマネージャーが最新の機能を見るためにページを更新する必要があり、最高のユーザー体験を提供するものではありません。
以下のコードでは、この機能を実装するために Amplify の GraphQL Subscription を使ってデータをサブスクライブします。
// components/FeaturesTable.js
import {
Button,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
View,
} from "@aws-amplify/ui-react";
import { API, graphqlOperation } from "aws-amplify";
import React, { useEffect, useState } from "react";
import { deleteFeature } from "../src/graphql/mutations";
import { listFeatures } from "../src/graphql/queries";
import { onCreateFeature, onDeleteFeature, onUpdateFeature, } from "../src/graphql/subscriptions";
function FeaturesTable({ initialFeatures = [], setActiveFeature }) {
const [features, setFeatures] = useState(initialFeatures);
useEffect(() => {
const fetchFeatures = async () => {
const result = await API.graphql(graphqlOperation(listFeatures));
setFeatures(result.data.listFeatures.items);
};
fetchFeatures();
const createSub = API.graphql(graphqlOperation(onCreateFeature)).subscribe({
next: ({ value }) => {
setFeatures((features) => [...features, value.data.onCreateFeature]);
},
});
const updateSub = API.graphql(graphqlOperation(onUpdateFeature)).subscribe({
next: ({ value }) => {
setFeatures((features) => {
const toUpdateIndex = features.findIndex(
(item) => item.id === value.data.onUpdateFeature.id
);
if (toUpdateIndex === -1) {
return [...features, value.data.onUpdateFeature];
}
return [
...features.slice(0, toUpdateIndex),
value.data.onUpdateFeature,
...features.slice(toUpdateIndex + 1),
];
});
},
});
const deleteSub = API.graphql(graphqlOperation(onDeleteFeature)).subscribe({
next: ({ value }) => {
setFeatures((features) => {
const toDeleteIndex = features.findIndex(
(item) => item.id === value.data.onDeleteFeature.id
);
return [
...features.slice(0, toDeleteIndex),
...features.slice(toDeleteIndex + 1),
];
});
},
});
return () => {
createSub.unsubscribe();
updateSub.unsubscribe();
deleteSub.unsubscribe();
};
}, []);
// remainder of FeaturesTable component unmodified
}
export default FeaturesTable;
ここでは、createSub
、updateSub
、deleteSub
Subscription を useEffect
に追加し、AppSync から onCreateFeature
、onUpdateFeature
、onDeleteFeature
のいずれかの Subscription
に対してプッシュされたデータの変更をリッスンします。
それぞれのクエリに対して、subscribe に渡されたオブジェクトの次の関数を介して、アプリケーションを更新するロジックを実装しなければなりません。createSub
は、setFeatures
を呼び出し、Subscription 経由で受け取ったレコードを渡すことで、features
に新しいレコードを追加します。 updateSub
は、変更されたレコードを検索し、features 内のレコードを Subscription から返されたバージョンに置き換えるコールバックを実装します。deleteSub
は、変更されたレコードを検索し、features
から削除するコールバックを実装します。 最後に、Subscription をクリーンアップするために、それぞれの Subscription の unsubscribe
メソッドへの呼び出しを返します。
管理画面でアイテムを作成したり編集したりすると、機能リストがすぐに更新されることがわかります。
本記事では、プロダクトマネージャーがロードマップに機能を追加するための管理インターフェイスを構築しました。次の記事では、ユーザー向けのページを実装し、その機能に関連する内部ドキュメントを保存する機能を追加します。
本記事は「Build a Product Roadmap with Next.js and Amplify」を翻訳したものです。