React から Smithy API へ
connection ジェネレータは、React ウェブサイトと Smithy TypeScript API バックエンドを迅速に統合する方法を提供します。タイプセーフな方法で Smithy API に接続するために必要なすべての設定(クライアントと TanStack Query フックの生成、AWS IAM および Cognito 認証のサポート、適切なエラーハンドリングなど)を行います。
このジェネレータを使用する前に、React アプリケーションが以下を満たしていることを確認してください:
- アプリケーションをレンダリングする
main.tsxファイルが存在すること - 動作する Smithy TypeScript API バックエンドが存在すること(
ts#smithy-apiジェネレータを使用して生成) - Cognito または IAM 認証を使用する API に接続する場合、
ts#react-website-authジェネレータで Cognito 認証が追加されていること
必要な main.tsx の構造の例
import { StrictMode } from 'react';import * as ReactDOM from 'react-dom/client';import App from './app/app';
const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement,);root.render( <StrictMode> <App /> </StrictMode>,);ジェネレータの実行
Section titled “ジェネレータの実行”- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - connection - 必須パラメータを入力
- クリック
Generate
pnpm nx g @aws/nx-plugin:connectionyarn nx g @aws/nx-plugin:connectionnpx nx g @aws/nx-plugin:connectionbunx nx g @aws/nx-plugin:connection| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
| sourceProject 必須 | string | - | ソース プロジェクト |
| targetProject 必須 | string | - | 接続先のターゲット プロジェクト |
| sourceComponent | string | - | 接続元のソース コンポーネント (コンポーネント名、ソース プロジェクト ルートからの相対パス、またはジェネレーター ID)。プロジェクトをソースとして明示的に選択するには '.' を使用します。 |
| targetComponent | string | - | 接続先のターゲット コンポーネント (コンポーネント名、ターゲット プロジェクト ルートからの相対パス、またはジェネレーター ID)。プロジェクトをターゲットとして明示的に選択するには '.' を使用します。 |
ジェネレータの出力
Section titled “ジェネレータの出力”ジェネレータは React アプリケーションの以下のファイルを変更します:
Directorysrc
Directorycomponents
- <ApiName>Provider.tsx APIクライアントのプロバイダ
- QueryClientProvider.tsx TanStack React Query クライアントプロバイダ
DirectoryRuntimeConfig/ ローカル開発用のランタイム設定コンポーネント
- …
Directoryhooks
- use<ApiName>.tsx TanStack Query で状態管理されたAPI呼び出し用フック
- use<ApiName>Client.tsx バニラAPIクライアントインスタンス生成用フック
- useSigV4.tsx IAM認証でSigV4によるHTTPリクエスト署名用フック(IAM認証選択時)
- project.json タイプセーフなクライアントを生成する新しいビルドターゲットが追加
- .gitignore 生成されたクライアントファイルはデフォルトで無視
ジェネレータは Smithy モデルにもファイルを追加します:
Directorymodel
Directorysrc
- extensions.smithy 生成クライアントをカスタマイズするためのトレート定義
また、ジェネレータはウェブサイトインフラにランタイム設定を追加します(存在しない場合)。これにより、Smithy API の API URL がウェブサイトで利用可能になり、use<ApiName>.tsx フックで自動的に設定されます。
ビルド時に、Smithy API の OpenAPI 仕様からタイプセーフなクライアントが生成されます。これにより React アプリケーションに3つの新しいファイルが追加されます:
Directorysrc
Directorygenerated
Directory<ApiName>
- types.gen.ts Smithyモデル構造から生成された型
- client.gen.ts API呼び出し用タイプセーフクライアント
- options-proxy.gen.ts TanStack Query を使用してAPIと対話するためのTanStack Query フックオプション生成メソッド
生成コードの使用方法
Section titled “生成コードの使用方法”生成されたタイプセーフクライアントはReactアプリからSmithy APIを呼び出すために使用できます。TanStack Queryフック経由での使用が推奨されますが、バニラクライアントも直接使用可能です。
APIフックの使用
Section titled “APIフックの使用”ジェネレータが提供するuse<ApiName>フックを使用して、TanStack QueryでAPIを呼び出せます。
queryOptionsメソッドでTanStack QueryのuseQueryフックに必要なオプションを取得できます:
import { useQuery } from '@tanstack/react-query';import { useState, useEffect } from 'react';import { useMyApi } from './hooks/useMyApi';
function MyComponent() { const api = useMyApi(); const item = useQuery(api.getItem.queryOptions({ itemId: 'some-id' }));
if (item.isLoading) return <div>Loading...</div>; if (item.isError) return <div>Error: {item.error.message}</div>;
return <div>Item: {item.data.name}</div>;}APIクライアントの直接使用
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function MyComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItem = async () => { try { const data = await api.getItem({ itemId: 'some-id' }); setItem(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItem(); }, [api]);
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
return <div>Item: {item.name}</div>;}ミューテーション
Section titled “ミューテーション”生成フックはTanStack QueryのuseMutationフックを使用したミューテーションをサポートします。作成、更新、削除操作をローディング状態、エラーハンドリング、楽観的更新とともに扱うクリーンな方法を提供します。
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // 生成されたミューテーションオプションを使用してミューテーションを作成 const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* フォームフィールド */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? '作成中...' : 'アイテム作成'} </button>
{createItem.isSuccess && ( <div className="success"> 作成されたアイテムID: {createItem.data.id} </div> )}
{createItem.isError && ( <div className="error"> エラー: {createItem.error.message} </div> )} </form> );}ミューテーション状態に応じたコールバックも追加可能です:
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // ミューテーション成功時に実行 console.log('アイテム作成成功:', data); // 新しいアイテムに遷移可能 navigate(`/items/${data.id}`); }, onError: (error) => { // ミューテーション失敗時に実行 console.error('アイテム作成失敗:', error); }, onSettled: () => { // ミューテーション完了時に実行(成功または失敗) // 影響を受ける可能性のあるクエリを無効化する適切な場所 queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});APIクライアントを直接使用したミューテーション
import { useState } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function CreateItemForm() { const api = useMyApiClient(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [createdItem, setCreatedItem] = useState(null);
const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(null);
try { const newItem = await api.createItem({ name: 'New Item', description: 'A new item' }); setCreatedItem(newItem); // 新しいアイテムに遷移可能 // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('アイテム作成失敗:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* フォームフィールド */} <button type="submit" disabled={isLoading} > {isLoading ? '作成中...' : 'アイテム作成'} </button>
{createdItem && ( <div className="success"> 作成されたアイテムID: {createdItem.id} </div> )}
{error && ( <div className="error"> エラー: {error.message} </div> )} </form> );}無限クエリによるページネーション
Section titled “無限クエリによるページネーション”cursorパラメータを入力として受け入れるエンドポイント向けに、生成フックはTanStack QueryのuseInfiniteQueryフックを使用した無限クエリをサポートします。これにより「さらに読み込む」や無限スクロール機能の実装が容易になります。
import { useInfiniteQuery } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemList() { const api = useMyApi(); const items = useInfiniteQuery({ ...api.listItems.infiniteQueryOptions({ limit: 10, // ページあたりのアイテム数 }, { // 次ページの 'cursor' として渡すパラメータを返す // getNextPageParam 関数を必ず定義してください getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* ページ配列をフラット化してすべてのアイテムをレンダリング */} <ul> {items.data.pages.flatMap(page => page.items.map(item => ( <li key={item.id}>{item.name}</li> )) )} </ul>
<button onClick={() => items.fetchNextPage()} disabled={!items.hasNextPage || items.isFetchingNextPage} > {items.isFetchingNextPage ? 'さらに読み込み中...' : items.hasNextPage ? 'さらに読み込む' : 'これ以上ありません'} </button> </div> );}生成フックはAPIがサポートしている場合、カーソルベースのページネーションを自動処理します。nextCursor値はレスポンスから抽出され次ページの取得に使用されます。
APIクライアントを直接使用したページネーション
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [nextCursor, setNextCursor] = useState(null); const [isFetchingMore, setIsFetchingMore] = useState(false);
// 初期データを取得 useEffect(() => { const fetchItems = async () => { try { setIsLoading(true); const response = await api.listItems({ limit: 10 }); setItems(response.items); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsLoading(false); } };
fetchItems(); }, [api]);
// さらにアイテムを読み込む関数 const loadMore = async () => { if (!nextCursor) return;
try { setIsFetchingMore(true); const response = await api.listItems({ limit: 10, cursor: nextCursor });
setItems(prevItems => [...prevItems, ...response.items]); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsFetchingMore(false); } };
if (isLoading) { return <LoadingSpinner />; }
if (error) { return <ErrorMessage message={error.message} />; }
return ( <div> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<button onClick={loadMore} disabled={!nextCursor || isFetchingMore} > {isFetchingMore ? '読み込み中...' : nextCursor ? 'さらに読み込む' : 'これ以上ありません'} </button> </div> );}エラーハンドリング
Section titled “エラーハンドリング”統合には型付きエラーレスポンスを持つ組み込みエラーハンドリングが含まれます。<operation-name>Error型が生成され、Smithyモデルで定義された可能性のあるエラーレスポンスをカプセル化します。各エラーにはstatusとerrorプロパティがあり、statusの値をチェックすることで特定のエラータイプに絞り込むことができます。
import { useMutation } from '@tanstack/react-query';
function MyComponent() { const api = useMyApi(); const createItem = useMutation(api.createItem.mutationOptions());
const handleClick = () => { createItem.mutate({ name: 'New Item' }); };
if (createItem.error) { switch (createItem.error.status) { case 400: // error.error は CreateItem400Response として型付け return ( <div> <h2>無効な入力:</h2> <p>{createItem.error.error.message}</p> </div> ); case 403: // error.error は CreateItem403Response として型付け return ( <div> <h2>権限なし:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error は CreateItem5XXResponse として型付け return ( <div> <h2>サーバーエラー:</h2> <p>{createItem.error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>アイテム作成</button>;}APIクライアントを直接使用したエラーハンドリング
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'New Item' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: // error.error は CreateItem400Response として型付け return ( <div> <h2>無効な入力:</h2> <p>{error.error.message}</p> </div> ); case 403: // error.error は CreateItem403Response として型付け return ( <div> <h2>権限なし:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error は CreateItem5XXResponse として型付け return ( <div> <h2>サーバーエラー:</h2> <p>{error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>アイテム作成</button>;}生成コードのカスタマイズ
Section titled “生成コードのカスタマイズ”ターゲットSmithy modelプロジェクトのextensions.smithyに一連のSmithyトレートが追加され、生成クライアントのカスタマイズに使用できます。
クエリとミューテーション
Section titled “クエリとミューテーション”デフォルトで、Smithy APIでHTTPメソッドPUT、POST、PATCH、DELETEを使用する操作はミューテーションとして扱われ、他はすべてクエリとして扱われます。
extensions.smithyのモデルプロジェクトに追加される@queryと@mutation Smithyトレートを使用してこの動作を変更できます。
@query
Section titled “@query”Smithy操作に@queryトレートを適用して、クエリとして扱うよう強制します:
@http(method: "POST", uri: "/items")@queryoperation ListItems { input: ListItemsInput output: ListItemsOutput}生成フックはPOST HTTPメソッドを使用していてもqueryOptionsを提供します:
const items = useQuery(api.listItems.queryOptions());@mutation
Section titled “@mutation”Smithy操作に@mutationトレートを適用して、ミューテーションとして扱うよう強制します:
@http(method: "GET", uri: "/start-processing")@mutationoperation StartProcessing { input: StartProcessingInput output: StartProcessingOutput}生成フックはGET HTTPメソッドを使用していてもmutationOptionsを提供します:
const startProcessing = useMutation(api.startProcessing.mutationOptions());カスタムページネーションカーソル
Section titled “カスタムページネーションカーソル”デフォルトで、生成フックはcursorという名前のパラメータを使用したカーソルベースのページネーションを想定します。extensions.smithyのモデルプロジェクトに追加される@cursorトレートを使用してこの動作をカスタマイズできます。
@cursorトレートにinputTokenを指定して、ページネーショントークンに使用する入力パラメータ名を変更します:
@http(method: "GET", uri: "/items")@cursor(inputToken: "nextToken")operation ListItems { input := { nextToken: String limit: Integer } output := { items: ItemList nextToken: String }}cursorという名前の入力パラメータを持つ操作に対してinfiniteQueryOptionsを生成したくない場合、カーソルベースのページネーションを無効化できます:
@cursor(enabled: false)operation ListItems { input := { // 'cursor'という名前の入力パラメータは、デフォルトでこの操作をページネーション操作として扱います cursor: String } output := { ... }}操作のグループ化
Section titled “操作のグループ化”生成されたフックとクライアントメソッドは、Smithy操作の@tagsトレートに基づいて自動的に整理されます。同じタグを持つ操作はグループ化され、API呼び出しを整理しやすくし、IDEでのコード補完が向上します。
例えば、このSmithyモデルの場合:
service MyService { operations: [ListItems, CreateItem, ListUsers, CreateUser]}
@tags(["items"])operation ListItems { input: ListItemsInput output: ListItemsOutput}
@tags(["items"])operation CreateItem { input: CreateItemInput output: CreateItemOutput}
@tags(["users"])operation ListUsers { input: ListUsersInput output: ListUsersOutput}
@tags(["users"])operation CreateUser { input: CreateUserInput output: CreateUserOutput}生成フックはタグごとにグループ化されます:
import { useQuery, useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() { const api = useMyApi();
// Items操作は api.items 配下にグループ化 const items = useQuery(api.items.listItems.queryOptions()); const createItem = useMutation(api.items.createItem.mutationOptions());
// Users操作は api.users 配下にグループ化 const users = useQuery(api.users.listUsers.queryOptions());
// 使用例 const handleCreateItem = () => { createItem.mutate({ name: 'New Item' }); };
return ( <div> <h2>Items</h2> <ul> {items.data?.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>アイテム追加</button>
<h2>Users</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}このグループ化により、API呼び出しを整理しやすくなり、IDEでのコード補完が向上します。
APIクライアントを直接使用したグループ化操作
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemsAndUsers() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true);
// データを読み込む useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Items操作は api.items 配下にグループ化 const itemsData = await api.items.listItems(); setItems(itemsData);
// Users操作は api.users 配下にグループ化 const usersData = await api.users.listUsers(); setUsers(usersData); } catch (error) { console.error('データ取得エラー:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // グループ化されたメソッドを使用してアイテムを作成 const newItem = await api.items.createItem({ name: 'New Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('アイテム作成エラー:', error); } };
if (isLoading) { return <div>読み込み中...</div>; }
return ( <div> <h2>Items</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>アイテム追加</button>
<h2>Users</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}Smithyモデルでカスタムエラー構造を定義することで、Smithy APIのエラーレスポンスをカスタマイズできます。生成クライアントはこれらのカスタムエラータイプを自動的に処理します。
カスタムエラー構造の定義
Section titled “カスタムエラー構造の定義”Smithyモデルでエラー構造を定義します:
@error("client")@httpError(400)structure InvalidRequestError { @required message: String
fieldErrors: FieldErrorList}
@error("client")@httpError(403)structure UnauthorizedError { @required reason: String}
@error("server")@httpError(500)structure InternalServerError { @required message: String
traceId: String}
list FieldErrorList { member: FieldError}
structure FieldError { @required field: String
@required message: String}操作へのエラー追加
Section titled “操作へのエラー追加”操作が返す可能性のあるエラーを指定します:
operation CreateItem { input: CreateItemInput output: CreateItemOutput errors: [ InvalidRequestError UnauthorizedError InternalServerError ]}
operation GetItem { input: GetItemInput output: GetItemOutput errors: [ ItemNotFoundError InternalServerError ]}
@error("client")@httpError(404)structure ItemNotFoundError { @required message: String}Reactでのカスタムエラータイプの使用
Section titled “Reactでのカスタムエラータイプの使用”生成クライアントはこれらのカスタムエラータイプを自動的に処理し、異なるエラーレスポンスの型チェックと処理が可能になります:
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// 型付きエラーハンドリングを伴うクエリ const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // エラーはSmithyモデルのエラーに基づいて型付け switch (error.status) { case 404: // error.error は ItemNotFoundError として型付け console.error('見つかりません:', error.error.message); break; case 500: // error.error は InternalServerError として型付け console.error('サーバーエラー:', error.error.message); console.error('トレースID:', error.error.traceId); break; } } });
// 型付きエラーハンドリングを伴うミューテーション const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error は InvalidRequestError として型付け console.error('バリデーションエラー:', error.error.message); console.error('フィールドエラー:', error.error.fieldErrors); break; case 403: // error.error は UnauthorizedError として型付け console.error('権限なし:', error.error.reason); break; } } });
// エラーハンドリングを伴うコンポーネントレンダリング if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error.message} />; } else if (getItem.error.status === 500) { return <ErrorMessage message={getItem.error.error.message} />; } }
return ( <div> {/* コンポーネントコンテンツ */} </div> );}クライアントを直接使用したカスタムエラーハンドリング
import { useState, useEffect } from 'react';
function ItemComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true);
// エラーハンドリングを伴うアイテム取得 useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // エラーはSmithyモデルのエラーに基づいて型付け const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error は ItemNotFoundError として型付け console.error('見つかりません:', err.error.message); break; case 500: // err.error は InternalServerError として型付け console.error('サーバーエラー:', err.error.message); console.error('トレースID:', err.error.traceId); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// エラーハンドリングを伴うアイテム作成 const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error は InvalidRequestError として型付け console.error('バリデーションエラー:', err.error.message); console.error('フィールドエラー:', err.error.fieldErrors); break; case 403: // err.error は UnauthorizedError として型付け console.error('権限なし:', err.error.reason); break; } } };
// エラーハンドリングを伴うコンポーネントレンダリング if (loading) { return <LoadingSpinner />; }
if (error) { if (error.status === 404) { return <NotFoundMessage message={error.error.message} />; } else if (error.status === 500) { return <ErrorMessage message={error.error.message} />; } }
return ( <div> {/* コンポーネントコンテンツ */} </div> );}ベストプラクティス
Section titled “ベストプラクティス”ローディング状態の処理
Section titled “ローディング状態の処理”より良いユーザーエクスペリエンスのため、ローディングとエラー状態を常に処理しましょう:
import { useQuery } from '@tanstack/react-query';
function ItemList() { const api = useMyApi(); const items = useQuery(api.listItems.queryOptions());
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { const err = items.error; switch (err.status) { case 403: // err.error は ListItems403Response として型付け return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error は ListItems5XXResponse として型付け return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="不明なエラーが発生しました" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}APIクライアントを直接使用したローディング状態処理
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItems = async () => { try { const data = await api.listItems(); setItems(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItems(); }, [api]);
if (loading) { return <LoadingSpinner />; }
if (error) { const err = error as ListItemsError; switch (err.status) { case 403: // err.error は ListItems403Response として型付け return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error は ListItems5XXResponse として型付け return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="不明なエラーが発生しました" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}より良いユーザーエクスペリエンスのため、楽観的更新を実装しましょう:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ItemList() { const api = useMyApi(); const queryClient = useQueryClient();
// アイテムを取得するクエリ const itemsQuery = useQuery(api.listItems.queryOptions());
// 楽観的更新を伴うアイテム削除のミューテーション const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // 進行中の再取得をキャンセル await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// 以前の値のスナップショット const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// 新しい値に楽観的に更新 queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// スナップショットを含むコンテキストオブジェクトを返す return { previousItems }; }, onError: (err, itemId, context) => { // ミューテーションが失敗した場合、onMutateから返されたコンテキストを使用してロールバック queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('アイテム削除失敗:', err); }, onSettled: () => { // エラーまたは成功後に常に再取得してサーバーとデータを同期 queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="アイテムの読み込みに失敗しました" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? '削除中...' : '削除'} </button> </li> ))} </ul> );}APIクライアントを直接使用した楽観的更新
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // 楽観的にアイテムを削除 const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // エラー時に以前のアイテムを復元 setItems(previousItems); console.error('アイテム削除失敗:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>削除</button> </li> ))} </ul> );}タイプセーフティ
Section titled “タイプセーフティ”統合は完全なエンドツーエンドのタイプセーフティを提供します。IDEはすべてのAPI呼び出しで完全なオートコンプリートと型チェックを提供します:
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// アイテム作成のためのタイプセーフなミューテーション const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ onSuccessコールバックが正しいレスポンスタイプを処理しない場合は型エラー onSuccess: (data) => { // data は API のレスポンススキーマに基づいて完全に型付け console.log(`作成されたアイテムID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ 入力がスキーマと一致しない場合は型エラー createItem.mutate(data); };
// エラーUIは型の絞り込みを使用して異なるエラータイプを処理可能 if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error は InvalidRequestError として型付け return ( <FormError message="無効な入力" errors={error.error.fieldErrors} /> ); case 403: // error.error は UnauthorizedError として型付け return <AuthError reason={error.error.reason} />; default: // error.error は 500 などの場合 InternalServerError として型付け return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> {/* フォームフィールド */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? '作成中...' : 'アイテム作成'} </button> </form> );}APIクライアントを直接使用したタイプセーフティ
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ 入力がスキーマと一致しない場合は型エラー await api.createItem(data); } catch (e) { // ✅ エラータイプにはすべての可能なエラーレスポンスが含まれる const err = e as CreateItemError; switch (err.status) { case 400: // err.error は InvalidRequestError として型付け console.error('バリデーションエラー:', err.error.fieldErrors); break; case 403: // err.error は UnauthorizedError として型付け console.error('権限なし:', err.error.reason); break; case 500: // err.error は InternalServerError として型付け console.error('サーバーエラー:', err.error.message); break; } setError(err); } };
// エラーUIは型の絞り込みを使用して異なるエラータイプを処理可能 if (error) { switch (error.status) { case 400: return ( <FormError message="無効な入力" errors={error.error.fieldErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}型はSmithy APIのOpenAPIスキーマから自動生成されるため、APIへの変更はビルド後にフロントエンドコードに反映されます。
Custom Auth
Section titled “Custom Auth”If your Smithy API uses Custom authentication (Lambda Authorizer), you will need to edit the generated client provider to add the authorization headers your authorizer expects. Look for the fetch configuration in the generated <ApiName>Provider.tsx and add your token or API key to the request headers.