以下の公式Amplifyドキュメントを参考に、GraphQL APIを利用してリソースの作成・表示・更新・削除の操作を行うReactのTodoアプリを作成しました。
なお、Amplifyのドキュメントは、CLIやライブラリ、ガイドなどの多数のカテゴリで分かれており、上部からそれぞれのカテゴリに移動できます。
注意点として、例えば一つのカテゴリに欲しい情報が載っていなくても、その他のカテゴリに載っていることが多々あります。
少し面倒ですが、それぞれのカテゴリを全て確認することをお勧めします。
なお、Todoアプリ作成にあたり使用したCLIやパッケージなどのバージョンなどは以下です。
$ sw_vers
ProductName: macOS
ProductVersion: 11.6
BuildVersion: 20G165
$ node -v
v18.0.0
$ npm -v
8.6.0
$ amplify --v
8.2.0
$ npm ls --depth=0
app-name@0.1.0 /xx/xx/aws/amplify/app-name
├── @testing-library/jest-dom@5.16.4
├── @testing-library/react@13.3.0
├── @testing-library/user-event@13.5.0
├── aws-amplify@4.3.24
├── react-dom@18.1.0
├── react-scripts@5.0.1
├── react@18.1.0
└── web-vitals@2.1.4
雛形のReactアプリ作成からAmplifyでGraphQL API追加まで
まず、雛形となるReactアプリを作成し、Amplifyで初期化します(デフォルト値)。
Amplify CLIで利用する認証情報などはお好みで。
$ npx create-react-app app-name
$ cd app-name
$ amplify init
amplify add apiコマンドを実行し、全てデフォルト値で設定します。
英語が実際のメッセージで、日本語は機械翻訳したものです。
補足で翻訳以外の内容を含んでいる箇所もあります。
$ amplify add api
// 以下のサービスからお選びください(GraphQLかRESTを選択)
? Select from one of the below mentioned services: GraphQL
❯ GraphQL
REST
// これから作成するGraphQL APIはこちらです。編集する設定を選択するか、続ける
// デフォルトの認可モードはAPI Keyです。API Keyを利用してGraphQL APIを利用できます。
? Here is the GraphQL API that we will create. Select a setting to edit or continue Continue
Name: basic
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)
❯ Single object with fields (e.g., “Todo” with ID, name, description)
One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”)
Blank Schema
// 警告: あなたのGraphQL APIは現在、API Keyを介してすべてのモデルへのパブリックな作成、読み取り、更新、および削除アクセスを許可しています。PRODUCTION-READYの認可ルールを設定するには、https://docs.amplify.aws/cli/graphql/authorization-rules をご覧ください。
// この意味は後述します。
⚠️ WARNING: your GraphQL API currently allows public create, read, update, and delete access to all models via an API Key. To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql/authorization-rules
// GraphQLスキーマのコンパイルに成功しました。
✅ GraphQL schema compiled successfully.
// スキーマを .../amplify/backend/api/basic/schema.graphql で編集するか、 .graphql ファイルを .../amplify/backend/api/basic/schema ディレクトリに配置してください。
Edit your schema at /xx/xx/aws/amplify/react-amplified/amplify/backend/api/basic/schema.graphql or place .graphql files in a directory at /xx/xx/aws/amplify/react-amplified/amplify/backend/api/basic/schema
// 今すぐスキーマを編集しますか?
✔ Do you want to edit the schema now? (Y/n) · yes
// エディターでファイルを編集する
Edit the file in your editor: /xx/xx/aws/amplify/react-amplified/amplify/backend/api/basic/schema.graphql
// ローカルにリソースベーシックを追加することに成功
✅ Successfully added resource basic locally
// 次のステップ
✅ Some next steps:
// すべてのローカルバックエンドリソースを構築し、クラウドにプロビジョニングします。
"amplify push" will build all your local backend resources and provision it in the cloud
// ローカルのバックエンドとフロントエンドのリソースをすべて構築し(ホスティングカテゴリが追加されている場合)、それをクラウドにプロビジョニングします。
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud
コマンドが終了すると、graphqlのスキーマファイルが開かれます。
// この「入力」は、このスキーマのすべてのモデルへのパブリックアクセスを可能にするグローバルな認可ルールを設定します。
// 認可ルールについて詳しくはこちら: https://docs.amplify.aws/cli/graphql/authorization-rules
# This "input" configures a global authorization rule to enable public access to
# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!
type Todo @model {
id: ID!
name: String!
description: String
}
上記で「ID!」などエクスクラメーションが付いているのは、Not Nullを表し、null値を許容しない必須フィールドです。
コマンド実行時、「…GraphQL APIは現在、API Keyを介してすべてのモデルへのパブリックな作成、読み取り、更新、および削除アクセスを許可しています。…」(機械翻訳)という内容が出力されましたが、そのパブリックなアクセスを許可しているのが上記schema.graphqlファイルの「input ~」の箇所です。
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!
上記ルールは、誰でもGraphQL APIを利用して作成・更新・削除などの操作を行えることを示します。
ただし、本当の意味でパブリックではなく、認可モードがAPI Keyのため、API Keyを知っている人なら誰でも上記操作を行うことができるという意味になります。
実際にローカルの変更をクラウドにデプロイします。
色々と聞かれますが、全てデフォルト値にしました。
$ amplify push
GraphQL API追加により作成されたAWSリソース
amplify pushを実行すると、CloudFormationにより様々なリソースが作成されました。
作成されたリソースを表にまとめると、以下になります。
CloudFormation | リソースタイプ | 概要 |
---|---|---|
amplify-basic-dev-xxx(amplifyのルートスタックに、右が追加された) | AWS::CloudFormation::Stack | このスタック(amplify-basic-dev-xxx-apibasic-xxx)によりGraphQL APIで利用される様々なリソースが作成される。 |
amplify-basic-dev-xxx-apibasic-xxx | AWS::AppSync::GraphQLApi | GraphQL APIを作成。 |
AWS::AppSync::ApiKey | APIキーを作成。 | |
AWS::AppSync::DataSource | DyanmoDBやLambdaなど、AppSyncのリゾルバーが接続するためのデータソースを作成するリソースタイプ。 このスタックで作成されるデータソースはDynamoDBなどのソースに接続されていないが、リゾルバーでは利用されている。 | |
AWS::AppSync::GraphQLSchema | スキーマを定義。 定義ファイルはS3DeploymentBucketにアップロードされている。このバケットは自アカウントになかったので、おそらくAWSが管理。 | |
AWS::CloudFormation::Stack | amplify-basic-dev-xxx-apibasic-xxx-CustomResourcesjson-xxx | |
AWS::CloudFormation::Stack | amplify-basic-dev-xxx-apibasic-xxx-Todo-xxx | |
amplify-basic-dev-xxx-apibasic-xxx-CustomResourcesjson-xxx | なし | 用途が分かりませんが、名前からすると何らかのカスタムリソースを作成するとこのスタックから作成されるのでしょうか? |
amplify-basic-dev-xxx-apibasic-xxx-Todo-xxx | AWS::DynamoDB::Table | API操作により変更される作成・削除・更新されるデータが保存されるDynamoDBを作成。 |
AWS::AppSync::DataSource | 上記DynamoDBとAppSyncが接続するためのデータソース。 | |
AWS::IAM::Role | 上記データソースのIAMロール。 | |
AWS::IAM::Policy | 上記IAMロールのポリシー。 DynamoDBを操作するための権限を持つ。 | |
AWS::AppSync::FunctionConfiguration | 特定の操作を実行するためのGraphQLの関数を定義。 | |
AWS::AppSync::Resolver | GraphQL APIのリクエストをどのデータソースに渡すかを決定する橋渡し。 |
完成ソースコード
GraphQL APIを利用するためのリソースの準備ができたので、アプリケーションのコードを編集します。
以下は、ボタンをクリックするとTodoを作成するReactアプリケーションです。
import React, { useState, useEffect } from 'react';
import './App.css';
import { Amplify, API, graphqlOperation } from 'aws-amplify';
import awsconfig from './aws-exports';
import { createTodo as createTodoMutation, updateTodo as updateTodoMutation, deleteTodo as deleteTodoMutation } from './graphql/mutations';
import { listTodos } from './graphql/queries';
Amplify.configure(awsconfig);
function App() {
const [todos, setTodos] = useState([]);
const [checkUseEffect, setCheckUseEffect] = useState(false)
const formState = {name: "", description: ""}
useEffect(() => {
console.log("useEffect実行")
fetchTodos();
},[checkUseEffect]);
async function fetchTodos(){
const apiData = await API.graphql(graphqlOperation(listTodos))
setTodos(apiData.data.listTodos.items);
}
async function createTodo(todo) {
console.log("createTodo関数実行")
await API.graphql(graphqlOperation(createTodoMutation, {input: todo}));
reloadTodoList();
}
async function updateTodo(id, name, description) {
console.log("updateTodo関数実行");
console.log(id)
console.log(name)
console.log(description)
await API.graphql(graphqlOperation(updateTodoMutation, {
input: {
id: id,
name: name,
description: description
}
}));
reloadTodoList();
}
async function deleteTodo(id){
console.log("deleteTodo関数開始");
await API.graphql(graphqlOperation(deleteTodoMutation, {
input: {
id: id
}
}))
reloadTodoList();
}
function reloadTodoList(){
setCheckUseEffect(!checkUseEffect);
}
function updateFormState(key, value){
formState[key] = value;
}
return (
<div className="App">
<header className="App-header">
<h1>Todoアプリ</h1>
<input placeholder="Todoのタイトル" onChange={e => updateFormState("name", e.target.value)} />
<input placeholder="Todoの説明" onChange={e => updateFormState("description", e.target.value)} />
<button onClick={() => createTodo(formState)}>Todo作成</button>
<h2>Todo一覧</h2>
<div style={{marginBottom: 30}}>
{
todos.map(todo => {
const input_name_id = todo.id + "name";
const input_description_id = todo.id + "description";
return (
<div key={todo.id}>
{
<div>
<input
id={input_name_id}
type="text"
defaultValue={todo.name}
/>
<input
id={input_description_id}
type="text"
defaultValue={todo.description}
/>
<button type="button" onClick={() => updateTodo(todo.id, document.getElementById(input_name_id).value, document.getElementById(input_description_id).value)}>更新</button>
<button type="button" onClick={() => deleteTodo(todo.id)}>削除</button>
</div>
}
</div>
)
})
}
</div>
</header>
</div>
);
}
export default App;
Amplifyのバージョンは更新頻度が高いため、Mutationなどの記載方法が変わることがよくあります。
以下の公式ドキュメントを都度確認して、現在の記載方法を確認すると良いでしょう。
必要なライブラリをインストールし、ローカルでReactを立ち上げます。
使用した実際のバージョンは本記事の冒頭に記載しています。
$ npm install aws-amplify
$ npm start
ボタンをクリックし、Todoを作成します。
クリックするとコンソールログが出力されたので、createTodo関数が実行されたことが分かります。
Graphql APIのリクエスト・レスポンスの情報
createTodo関数実行時、Graphql APIのリクエスト・レスポンスがどのように送られているか確認してみました。
Chromeの開発者ツールのネットワークタブから確認し、リクエストやレスポンスの情報をまとめたものが以下です。
項目 | 値 | 補足 |
---|---|---|
Request URL | https://xxx.appsync-api.ap-northeast-1.amazonaws.com/graphql | GraphQL APIのリクエスト先であるAppSyncのAPI URL |
Request Method | POST | Todoを作成するのでPOST |
Status Code | 200 | |
Remote Address | 18.65.166.36:443 | リクエスト先のIPアドレス。AWS IPアドレスの範囲に該当。Whoisで調べるとCloudFrontっぽい。 |
Referrer Policy | strict-origin-when-cross-origin | Referrerをリクエストに含めるかについて制御。クロスオリジンの場合、プロトコルのセキュリティ基準(HTTPS)が同じ場合、オリジンを送信。 |
項目 | 値 | 補足 |
---|---|---|
:authority | xxx.appsync-api.ap-northeast-1.amazonaws.com | URIの構成要素をうちオーソリティを表示(リクエストURLを部品ごとに分割しているだけ) |
:method | POST | 同上 |
:path | /graphql | 同上 |
:scheme | https | 同上 |
accept | application/json, text/plain, */* | クライアントが理解できるコンテンツタイプを伝える。最後に*で指定しているので、全てのコンテンツタイプを伝えている。 |
accept-encoding | gzip, deflate, br | コンテンツのエンコード(圧縮アルゴリズム)のどれをクライアントが理解できるか。 |
accept-language | ja,en-US;q=0.9,en;q=0.8 | |
cache-control | no-cache | キャッシュを制御する。 |
content-length | 297 | |
content-type | application/json; charset=UTF-8 | |
origin | http://localhost:3000 | どこがフェッチの原点かを示す(リクエストの大元)。 |
pragma | no-cache | |
referer | http://localhost:3000/ | 現在リクエストされているページへのリンク先を持った直前のWebページのアドレス。 |
sec-ch-ua | ” Not A;Brand”;v=”99″, “Chromium”;v=”101″, “Google Chrome”;v=”101″ | ユーザーのエージェントのブランドとバージョン情報などのヒント。 |
sec-ch-ua-mobile | ?0 | モバイルデバイスであるか否か。 |
sec-ch-ua-platform | “macOS” | ユーザーエージェントのOSやプラットフォームなど。 |
sec-fetch-dest | empty | リクエスト先の情報を示す。サーバーがリクエストがどのように使用されることが期待されるか判断できる。 |
sec-fetch-mode | cors | リクエストモードがわかる。同一オリジンか、異なるオリジンか、WebSocketか、など。 |
sec-fetch-site | cross-site | リクエストが同じオリジンか、別のオリジンかなどの情報をサーバーに通知。 |
user-agent | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 | 将来的に削除される |
x-amz-user-agent | aws-amplify/4.5.3 js | AWS関連のクライアントのブランドとバージョン情報 |
x-api-key | xxx | GraphQL APIリクエストに必要なAPI Key。 |
項目 | 値 | 補足 |
---|---|---|
access-control-allow-origin | * | 指定したオリジンからのリクエストを許可する。 |
access-control-expose-headers | x-amzn-RequestId,x-amzn-ErrorType,x-amz-user-agent,x-amzn-ErrorMessage,Date,x-amz-schema-version | CORSセーフリストレスポンスヘッダーに含まれないが、公開する必要があるヘッダーを指定。 |
content-length | 195 | |
content-type | application/json;charset=UTF-8 | |
date | Mon, 23 May 2022 08:14:15 GMT | |
via | 1.1 xxx.cloudfront.net (CloudFront) | 転送時に追加される。 |
x-amz-cf-id | xxx | CloudFrontへのリクエストを一意に識別する識別子。AWSサポート問い合わせ時に重宝しそう。 |
x-amz-cf-pop | NRT57-P1 | CloudFrontのエッジロケーション。NRTは空港コードで成田空港を指す。 |
x-amzn-appsync-tokensconsumed | 1 | リクエストで消費されたトークンの数。通常、1リクエスト1トークンだが、リソース(処理時間とメモリ消費量)を大量に消費するリクエストは、追加でトークンが割り当てられる。 |
x-amzn-requestid | xxx | リクエストを一意に識別するもの |
x-cache | Miss from cloudfront | キャッシュをから取得していないこと。AWS特有ではない。 |
{
"query": "mutation CreateTodo($input: CreateTodoInput!, $condition: ModelTodoConditionInput) {\n createTodo(input: $input, condition: $condition) {\n id\n name\n description\n createdAt\n updatedAt\n }\n}\n",
"variables": {
"input": {
"name": "SAP試験",
"description": "6/10試験日"
}
}
}
{
"data": {
"createTodo": {
"id": "817bc40f-9981-4c8e-aa8e-5acadf6e50dd",
"name": "SAP試験",
"description": "6/10試験日",
"createdAt": "2022-06-04T12:14:21.367Z",
"updatedAt": "2022-06-04T12:14:21.367Z"
}
}
}
一通り確認しましたが、AWS特有の拡張ヘッダーはあれど、Amplify(正確にはAppSync)特有のヘッダーは「x-amzn-appsync-tokensconsumed」くらいでした。
そのほかに特筆するヘッダーを挙げるなら、AppSync APIの認証モードがAPI Keyの場合に必須であるAPI Keyくらいでしょうか。
AWS特有の拡張ヘッダーがあるものの、ほとんど一般的なリクエスト内容でGraphQL APIをリクエストしていることが分かりました。
その他にアプリケーションを操作してみる(更新・削除)
以下の動画では、作成したTodoリストを「更新」ボタンにより更新した後、本当に更新できたかリロードして確認しています。
その後、新たなTodoを作成し、作成したTodoを削除しています。
コード解説
Graphql APIでTodoの表示・作成・更新・削除をどのように実装したか解説します。
基本的に以下のAWSの公式チュートリアルを参考にして作成しました。

作成
該当コード抜粋。
import { createTodo as createTodoMutation, updateTodo as updateTodoMutation, deleteTodo as deleteTodoMutation } from './graphql/mutations';
const formState = {name: "", description: ""}
async function createTodo(todo) {
console.log("createTodo関数実行")
await API.graphql(graphqlOperation(createTodoMutation, {input: todo}));
reloadTodoList();
}
function updateFormState(key, value){
formState[key] = value;
}
<input placeholder="Todoのタイトル" onChange={e => updateFormState("name", e.target.value)} />
<input placeholder="Todoの説明" onChange={e => updateFormState("description", e.target.value)} />
<button onClick={() => createTodo(formState)}>Todo作成</button>
作成するTodoの名前と説明はinputフォームに入力します。
入力->onChangeにより入力した文字列を引数としてupdateFormState関数実行->formState変数が更新される->Todo作成準備完了 の流れとなります。
あとは「Todo作成」ボタンをクリックすると入力した情報(formState変数)を引数としてcreateTodo関数を実行し、Todoを作成しています。
なお、createTodo関数でreloadTodoList関数を呼び出しています。reloadTodoList関数はTodoの更新をする関数等からも呼び出されています。詳細は後述しますが、このreloadTodoList関数によりTodo作成・更新・削除後に、その結果をすぐに画面に反映するようにしています。
Amplifyドキュメント – ガイド – フォームAPIの構築
表示
該当コード抜粋。
import { listTodos } from './graphql/queries';
const [todos, setTodos] = useState([]);
const [checkUseEffect, setCheckUseEffect] = useState(false)
useEffect(() => {
console.log("useEffect実行")
fetchTodos();
},[checkUseEffect]);
async function fetchTodos(){
const apiData = await API.graphql(graphqlOperation(listTodos))
setTodos(apiData.data.listTodos.items);
}
{
todos.map(todo => {
const input_name_id = todo.id + "name";
const input_description_id = todo.id + "description";
return (
<div key={todo.id}>
{
<div>
<input
id={input_name_id}
type="text"
defaultValue={todo.name}
/>
<input
id={input_description_id}
type="text"
defaultValue={todo.description}
/>
<button type="button" onClick={() => updateTodo(todo.id, document.getElementById(input_name_id).value, document.getElementById(input_description_id).value)}>更新</button>
<button type="button" onClick={() => deleteTodo(todo.id)}>削除</button>
</div>
}
</div>
)
})
}
useStateとuseEffectはReactで利用できる機能であり、詳細は後述の別記事にまとめていますので、気になる方はご覧ください。
useStateでステート変数であるtodosを作成し、このtodosの中にTodoリストを格納しています。
その格納作業を行なっているのが、fetchTodos関数です。
この関数の中でGraphQL APIにより入手したTodoリストを、setTodos関数を実行してtodos変数に格納しています。
fetchTodos関数はuseEffectの中で呼び出しています。
useEffectは画面レンダリング後に、第一引数に指定した処理が実行されます。
第二引数で実行タイミングを制御でき、今回はTodoリストが作成・更新・削除された際に第一引数の処理を実行するようにしています。
つまり、Todoリストに何か変更がある度にfetchTodos関数が呼び出され、最新のTodoリストが画面に表示されます。
実際に画面に表示しているのが、todos.mapの箇所です。
Todoリストを一つずつ取り出し、名前と説明をInputフォームのデフォルト文字列にして画面に表示しています。
更新
該当コード抜粋。
import { createTodo as createTodoMutation, updateTodo as updateTodoMutation, deleteTodo as deleteTodoMutation } from './graphql/mutations';
async function updateTodo(id, name, description) {
console.log("updateTodo関数実行");
console.log(id)
console.log(name)
console.log(description)
await API.graphql(graphqlOperation(updateTodoMutation, {
input: {
id: id,
name: name,
description: description
}
}));
reloadTodoList();
}
<input
id={input_name_id}
type="text"
defaultValue={todo.name}
/>
<input
id={input_description_id}
type="text"
defaultValue={todo.description}
/>
<button type="button" onClick={() => updateTodo(todo.id, document.getElementById(input_name_id).value, document.getElementById(input_description_id).value)}>更新</button>
Todoリスト一覧はinputフォームで画面上に表示されています。
各Todoには対応する更新ボタンが右横に付いており、その更新ボタンをクリックすると、現在inputフォームに入力されている値などを引数として、updateTodo関数を実行します。
inputフォームに入力されている値を取得する方法として、document.getElementByIdではなくReactの機能を使用する方法がないか試しましたが、方法がわからなかったためdocument.getelementByIdを利用しています。
削除
該当コード抜粋。
import { createTodo as createTodoMutation, updateTodo as updateTodoMutation, deleteTodo as deleteTodoMutation } from './graphql/mutations';
async function deleteTodo(id){
console.log("deleteTodo関数開始");
await API.graphql(graphqlOperation(deleteTodoMutation, {
input: {
id: id
}
}))
reloadTodoList();
}
<input
id={input_name_id}
type="text"
defaultValue={todo.name}
/>
<input
id={input_description_id}
type="text"
defaultValue={todo.description}
/>
<button type="button" onClick={() => deleteTodo(todo.id)}>削除</button>
削除は最も単純で、TodoのIDを引数としてdeleteTodo関数を実行し、Todoの削除を行なっています。
useEffectについて
上述した通り、useEffectの第二引数により、画面のレンダリング後のTodoリスト更新タイミングを制御しています。
その制御に利用したコードを抜粋すると以下です。
import React, { useState, useEffect } from 'react';
const [checkUseEffect, setCheckUseEffect] = useState(false)
useEffect(() => {
console.log("useEffect実行")
fetchTodos();
},[checkUseEffect]);
function reloadTodoList(){
setCheckUseEffect(!checkUseEffect);
}
useEffectは、第二引数で渡したものが前回と一致しているか比較し、一致していない場合にのみ第一引数を実行します。
上記コードの場合、ステート変数checkUseEffectが前回と一致しているか比較し、一致している場合は第一引数を実行せず、一致していない場合に第一引数を実行します。
第一引数ではTodoリストの更新をしていますので、更新が必要な場合にのみ処理を行うようにしたいです。
このTodoアプリでTodoリストの更新が必要な場合は、Todoリストが変更されたとき、つまり作成・更新・削除の操作が行われたタイミングです。
そのため、作成・更新・削除の操作を行う際に、reloadTodoList関数を実行し、Todoリストを更新する処理をしています。
reloadTodoList関数では、ステート変数checkUseEffectを反転する処理だけを行なっています。
ステート変数checkUseEffectは初期値がfalseのため、trueかfalseの値を持つことになります。
作成・削除・更新操作のたびに値の反転を行なっているので、各操作のたびに前回と異なる値を持つことになり、Todoリストを更新する処理が実行されています。
また、ステート変数checkUseEffectの反転操作を行うのは作成・削除・更新操作を実行する場合に限るので、各操作を実行するとき以外は、Todoリスト更新の処理(useEffect第一引数の処理)が行われません。
無限ループについて
useEffectの第二引数に渡すものによっては、無限ループが引き起こされるため注意が必要です。
例えば以下のように設定すると、無限ループが引き起こされます。
useEffect(() => {
fetchTodos();
},[todos]);
async function fetchTodos(){
const apiData = await API.graphql(graphqlOperation(listTodos))
setTodos(apiData.data.listTodos.items);
}
おそらくtodosの中身が一緒でも、異なるオブジェクトと判定されるからです。
比較されるtodosは、前々回SetTodosにより更新されたtodosであり、直近でSetTodosにより更新されたtodosとは内容が同じでも別物だからです。
無限ループを防ぐための方法はいろいろあると思いますが、今回は第二引数に使用するステート変数を作成して対応しました。
さいごに
AmplifyでGraphQL APIを利用して簡単なTodoアプリを作成してみました。
GraphQL APIの動作確認が主目的なので粗が色々とありますが、基本的な動作が確認できてよかったです。