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 | - | The source project |
| targetProject 必須 | string | - | The target project to connect to |
ジェネレータの出力
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への変更はビルド後にフロントエンドコードに反映されます。