React vers API Smithy
Le générateur connection fournit un moyen d’intégrer rapidement votre site web React avec votre backend d’API TypeScript Smithy. Il configure toute la configuration nécessaire pour se connecter à votre API Smithy de manière type-safe, y compris la génération de client et de hooks TanStack Query, la prise en charge de l’authentification AWS IAM et Cognito et la gestion appropriée des erreurs.
Prérequis
Section intitulée « Prérequis »Avant d’utiliser ce générateur, assurez-vous que votre application React dispose de :
- Un fichier
main.tsxqui rend votre application - Un backend d’API TypeScript Smithy fonctionnel (généré à l’aide du générateur
ts#apiavec--framework=smithy) - Cognito Auth ajouté via le générateur
ts#website#authsi vous connectez une API qui utilise l’authentification Cognito ou IAM
Exemple de structure main.tsx requise
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>,);Utilisation
Section intitulée « Utilisation »Exécuter le générateur
Section intitulée « Exécuter le générateur »pnpm nx g @aws/nx-plugin:connectionyarn nx g @aws/nx-plugin:connectionnpx nx g @aws/nx-plugin:connectionbunx nx g @aws/nx-plugin:connectionVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:connection --dry-runyarn nx g @aws/nx-plugin:connection --dry-runnpx nx g @aws/nx-plugin:connection --dry-runbunx nx g @aws/nx-plugin:connection --dry-run- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - connection - Remplissez les paramètres requis
- Cliquez sur
Generate
| Paramètre | Type | Par défaut | Description |
|---|---|---|---|
| sourceProject Requis | string | - | Le projet source |
| targetProject Requis | string | - | Le projet cible auquel se connecter |
| sourceComponent | string | - | Le composant source depuis lequel se connecter (nom du composant, chemin relatif à la racine du projet source, ou identifiant du générateur). Utilisez '.' pour sélectionner explicitement le projet comme source. |
| targetComponent | string | - | Le composant cible auquel se connecter (nom du composant, chemin relatif à la racine du projet cible, ou identifiant du générateur). Utilisez '.' pour sélectionner explicitement le projet comme cible. |
| preferInstallDependencies | boolean | true | Indique s'il faut privilégier l'installation des dépendances après l'exécution du générateur. Définir à false pour différer l'installation lors de l'exécution de plusieurs générateurs en lot (une installation s'exécute quand même si nécessaire pour que les générateurs suivants puissent calculer le graphe de projet Nx) ; installer une seule fois à la fin. |
Sortie du générateur
Section intitulée « Sortie du générateur »Le générateur apportera des modifications aux fichiers suivants dans votre application React :
Répertoiresrc
Répertoirecomponents
- <ApiName>Provider.tsx Provider pour votre client API
- QueryClientProvider.tsx Provider de client TanStack React Query
RépertoireRuntimeConfig/ Composant de configuration d’exécution pour le développement local
- …
Répertoirehooks
- use<ApiName>.tsx Ajoute un hook pour appeler votre API avec l’état géré par TanStack Query
- use<ApiName>Client.tsx Ajoute un hook pour instancier le client API vanilla qui peut appeler votre API.
- useSigV4.tsx Ajoute un hook pour signer les requêtes HTTP avec SigV4 (si vous avez sélectionné l’authentification IAM)
- project.json Une nouvelle cible est ajoutée au build qui génère un client type-safe
- .gitignore Les fichiers client générés sont ignorés par défaut
Le générateur ajoutera également un fichier à votre modèle Smithy :
Répertoiremodel
Répertoiresrc
- extensions.smithy Définit les traits qui peuvent être utilisés pour personnaliser le client généré
Le générateur ajoutera également Runtime Config à votre infrastructure de site web si elle n’est pas déjà présente, ce qui garantit que l’URL de l’API pour votre API Smithy est disponible dans le site web et automatiquement configurée par le hook use<ApiName>.tsx.
Génération de code
Section intitulée « Génération de code »Au moment de la compilation, un client type-safe est généré à partir de la spécification OpenAPI de votre API Smithy. Cela ajoutera trois nouveaux fichiers à votre application React :
Répertoiresrc
Répertoiregenerated
Répertoire<ApiName>
- types.gen.ts Types générés à partir des structures du modèle Smithy
- client.gen.ts Client type-safe pour appeler votre API
- options-proxy.gen.ts Fournit des méthodes pour créer des options de hooks TanStack Query pour interagir avec votre API en utilisant TanStack Query
Utilisation du code généré
Section intitulée « Utilisation du code généré »Le client type-safe généré peut être utilisé pour appeler votre API Smithy depuis votre application React. Il est recommandé d’utiliser le client via les hooks TanStack Query, mais vous pouvez utiliser le client vanilla si vous préférez.
Utilisation du hook API
Section intitulée « Utilisation du hook API »Le générateur fournit un hook use<ApiName> que vous pouvez utiliser pour appeler votre API avec TanStack Query.
Requêtes
Section intitulée « Requêtes »Vous pouvez utiliser la méthode queryOptions pour récupérer les options requises pour appeler votre API en utilisant le hook useQuery de TanStack Query :
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>;}Utilisation directe du client 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>;}Mutations
Section intitulée « Mutations »Les hooks générés incluent la prise en charge des mutations à l’aide du hook useMutation de TanStack Query. Cela fournit un moyen propre de gérer les opérations de création, de mise à jour et de suppression avec des états de chargement, la gestion des erreurs et les mises à jour optimistes.
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // Create a mutation using the generated mutation options const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creating...' : 'Create Item'} </button>
{createItem.isSuccess && ( <div className="success"> Item created with ID: {createItem.data.id} </div> )}
{createItem.isError && ( <div className="error"> Error: {createItem.error.message} </div> )} </form> );}Vous pouvez également ajouter des callbacks pour différents états de mutation :
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // This will run when the mutation succeeds console.log('Item created:', data); // You can navigate to the new item navigate(`/items/${data.id}`); }, onError: (error) => { // This will run when the mutation fails console.error('Failed to create item:', error); }, onSettled: () => { // This will run when the mutation completes (success or error) // Good place to invalidate queries that might be affected queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});Mutations utilisant directement le client 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); // You can navigate to the new item // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Failed to create item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <button type="submit" disabled={isLoading} > {isLoading ? 'Creating...' : 'Create Item'} </button>
{createdItem && ( <div className="success"> Item created with ID: {createdItem.id} </div> )}
{error && ( <div className="error"> Error: {error.message} </div> )} </form> );}Pagination avec requêtes infinies
Section intitulée « Pagination avec requêtes infinies »Pour les endpoints qui acceptent un paramètre cursor en entrée, les hooks générés fournissent la prise en charge des requêtes infinies à l’aide du hook useInfiniteQuery de TanStack Query. Cela facilite l’implémentation de la fonctionnalité “charger plus” ou de défilement infini.
import { useInfiniteQuery } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemList() { const api = useMyApi(); const items = useInfiniteQuery({ ...api.listItems.infiniteQueryOptions({ limit: 10, // Number of items per page }, { // Make sure you define a getNextPageParam function to return // the parameter that should be passed as the 'cursor' for the // next page getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* Flatten the pages array to render all items */} <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 ? 'Loading more...' : items.hasNextPage ? 'Load More' : 'No more items'} </button> </div> );}Les hooks générés gèrent automatiquement la pagination basée sur le curseur si votre API la prend en charge. La valeur nextCursor est extraite de la réponse et utilisée pour récupérer la page suivante.
Pagination utilisant directement le client 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);
// Fetch initial data 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]);
// Function to load more items 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 ? 'Loading more...' : nextCursor ? 'Load More' : 'No more items'} </button> </div> );}Gestion des erreurs
Section intitulée « Gestion des erreurs »L’intégration inclut une gestion des erreurs intégrée avec des réponses d’erreur typées. Un type <operation-name>Error est généré qui encapsule les réponses d’erreur possibles définies dans le modèle Smithy. Chaque erreur a une propriété status et error, et en vérifiant la valeur de status, vous pouvez réduire à un type d’erreur spécifique.
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 is typed as CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{createItem.error.error.message}</p> </div> ); case 403: // error.error is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{createItem.error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}Gestion des erreurs utilisant directement le client 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 is typed as CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{error.error.message}</p> </div> ); case 403: // error.error is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}Personnalisation du code généré
Section intitulée « Personnalisation du code généré »Une sélection de traits Smithy est ajoutée à votre projet Smithy model cible dans extensions.smithy que vous pouvez utiliser pour personnaliser le client généré.
Requêtes et mutations
Section intitulée « Requêtes et mutations »Par défaut, les opérations de votre API Smithy qui utilisent les méthodes HTTP PUT, POST, PATCH et DELETE sont considérées comme des mutations, et toutes les autres sont considérées comme des requêtes.
Vous pouvez modifier ce comportement en utilisant les traits Smithy @query et @mutation qui sont ajoutés à votre projet de modèle dans extensions.smithy.
Appliquez ensuite le trait @query à votre opération Smithy pour la forcer à être traitée comme une requête :
@http(method: "POST", uri: "/items")@queryoperation ListItems { input: ListItemsInput output: ListItemsOutput}Le hook généré fournira queryOptions même s’il utilise la méthode HTTP POST :
const items = useQuery(api.listItems.queryOptions());@mutation
Section intitulée « @mutation »Appliquez le trait @mutation à votre opération Smithy pour la forcer à être traitée comme une mutation :
@http(method: "GET", uri: "/start-processing")@mutationoperation StartProcessing { input: StartProcessingInput output: StartProcessingOutput}Le hook généré fournira mutationOptions même s’il utilise la méthode HTTP GET :
const startProcessing = useMutation(api.startProcessing.mutationOptions());Curseur de pagination personnalisé
Section intitulée « Curseur de pagination personnalisé »Par défaut, les hooks générés supposent une pagination basée sur le curseur avec un paramètre nommé cursor. Vous pouvez personnaliser ce comportement en utilisant le trait @cursor qui est ajouté à votre projet de modèle dans extensions.smithy.
Appliquez le trait @cursor avec inputToken pour modifier le nom du paramètre d’entrée utilisé pour le jeton de pagination :
@http(method: "GET", uri: "/items")@cursor(inputToken: "nextToken")operation ListItems { input := { nextToken: String limit: Integer } output := { items: ItemList nextToken: String }}Si vous ne souhaitez pas générer infiniteQueryOptions pour une opération qui a un paramètre d’entrée nommé cursor, vous pouvez désactiver la pagination basée sur le curseur :
@cursor(enabled: false)operation ListItems { input := { // Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default cursor: String } output := { ... }}Regroupement des opérations
Section intitulée « Regroupement des opérations »Les hooks et méthodes client générés sont automatiquement organisés en fonction du trait @tags dans vos opérations Smithy. Les opérations avec les mêmes tags sont regroupées, ce qui aide à garder vos appels d’API organisés et fournit une meilleure complétion de code dans votre IDE.
Par exemple, avec ce modèle 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}Les hooks générés seront regroupés par tags :
import { useQuery, useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() { const api = useMyApi();
// Items operations are grouped under api.items const items = useQuery(api.items.listItems.queryOptions()); const createItem = useMutation(api.items.createItem.mutationOptions());
// Users operations are grouped under api.users const users = useQuery(api.users.listUsers.queryOptions());
// Usage example 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}>Add Item</button>
<h2>Users</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}Ce regroupement facilite l’organisation de vos appels d’API et fournit une meilleure complétion de code dans votre IDE.
Opérations regroupées utilisant directement le client 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);
// Load data useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Items operations are grouped under api.items const itemsData = await api.items.listItems(); setItems(itemsData);
// Users operations are grouped under api.users const usersData = await api.users.listUsers(); setUsers(usersData); } catch (error) { console.error('Error fetching data:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Create item using the grouped method const newItem = await api.items.createItem({ name: 'New Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Error creating item:', error); } };
if (isLoading) { return <div>Loading...</div>; }
return ( <div> <h2>Items</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}Vous pouvez personnaliser les réponses d’erreur dans votre API Smithy en définissant des structures d’erreur personnalisées dans votre modèle Smithy. Le client généré gérera automatiquement ces types d’erreur personnalisés.
Définition de structures d’erreur personnalisées
Section intitulée « Définition de structures d’erreur personnalisées »Définissez vos structures d’erreur dans votre modèle 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}Ajout d’erreurs aux opérations
Section intitulée « Ajout d’erreurs aux opérations »Spécifiez quelles erreurs vos opérations peuvent retourner :
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}Utilisation de types d’erreur personnalisés dans React
Section intitulée « Utilisation de types d’erreur personnalisés dans React »Le client généré gérera automatiquement ces types d’erreur personnalisés, vous permettant de vérifier le type et de gérer différentes réponses d’erreur :
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// Query with typed error handling const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // Error is typed based on the errors in your Smithy model switch (error.status) { case 404: // error.error is typed as ItemNotFoundError console.error('Not found:', error.error.message); break; case 500: // error.error is typed as InternalServerError console.error('Server error:', error.error.message); console.error('Trace ID:', error.error.traceId); break; } } });
// Mutation with typed error handling const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error is typed as InvalidRequestError console.error('Validation error:', error.error.message); console.error('Field errors:', error.error.fieldErrors); break; case 403: // error.error is typed as UnauthorizedError console.error('Unauthorized:', error.error.reason); break; } } });
// Component rendering with error handling 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> {/* Component content */} </div> );}Gestion des erreurs personnalisées avec le client directement
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);
// Fetch item with error handling useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // Error is typed based on the errors in your Smithy model const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error is typed as ItemNotFoundError console.error('Not found:', err.error.message); break; case 500: // err.error is typed as InternalServerError console.error('Server error:', err.error.message); console.error('Trace ID:', err.error.traceId); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Create item with error handling const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error is typed as InvalidRequestError console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.fieldErrors); break; case 403: // err.error is typed as UnauthorizedError console.error('Unauthorized:', err.error.reason); break; } } };
// Component rendering with error handling 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> {/* Component content */} </div> );}Bonnes pratiques
Section intitulée « Bonnes pratiques »Gérer les états de chargement
Section intitulée « Gérer les états de chargement »Gérez toujours les états de chargement et d’erreur pour une meilleure expérience utilisateur :
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 is typed as ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}Gérer les états de chargement utilisant directement le client 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 is typed as ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}Mises à jour optimistes
Section intitulée « Mises à jour optimistes »Implémentez des mises à jour optimistes pour une meilleure expérience utilisateur :
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ItemList() { const api = useMyApi(); const queryClient = useQueryClient();
// Query to fetch items const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutation for deleting items with optimistic updates const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Snapshot the previous value const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Optimistically update to the new value queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Return a context object with the snapshot return { previousItems }; }, onError: (err, itemId, context) => { // If the mutation fails, use the context returned from onMutate to roll back queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Failed to delete item:', err); }, onSettled: () => { // Always refetch after error or success to ensure data is in sync with server queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="Failed to load items" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Deleting...' : 'Delete'} </button> </li> ))} </ul> );}Mises à jour optimistes utilisant directement le client API
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Optimistically remove the item const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restore previous items on error setItems(previousItems); console.error('Failed to delete item:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Delete</button> </li> ))} </ul> );}Sécurité des types
Section intitulée « Sécurité des types »L’intégration fournit une sécurité des types de bout en bout complète. Votre IDE fournira une autocomplétion complète et une vérification de type pour tous vos appels d’API :
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// Type-safe mutation for creating items const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ Type error if onSuccess callback doesn't handle the correct response type onSuccess: (data) => { // data is fully typed based on your API's response schema console.log(`Item created with ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Type error if input doesn't match schema createItem.mutate(data); };
// Error UI can use type narrowing to handle different error types if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error is typed as InvalidRequestError return ( <FormError message="Invalid input" errors={error.error.fieldErrors} /> ); case 403: // error.error is typed as UnauthorizedError return <AuthError reason={error.error.reason} />; default: // error.error is typed as InternalServerError for 500, etc. return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> {/* Form fields */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creating...' : 'Create Item'} </button> </form> );}Sécurité des types utilisant directement le client API
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Type error if input doesn't match schema await api.createItem(data); } catch (e) { // ✅ Error type includes all possible error responses const err = e as CreateItemError; switch (err.status) { case 400: // err.error is typed as InvalidRequestError console.error('Validation errors:', err.error.fieldErrors); break; case 403: // err.error is typed as UnauthorizedError console.error('Not authorized:', err.error.reason); break; case 500: // err.error is typed as InternalServerError console.error('Server error:', err.error.message); break; } setError(err); } };
// Error UI can use type narrowing to handle different error types if (error) { switch (error.status) { case 400: return ( <FormError message="Invalid input" errors={error.error.fieldErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}Les types sont automatiquement générés à partir du schéma OpenAPI de votre API Smithy, garantissant que toute modification de votre API est reflétée dans votre code frontend après une compilation.
Authentification personnalisée
Section intitulée « Authentification personnalisée »Si votre API Smithy utilise l’authentification Custom (Lambda Authorizer), vous devrez modifier le provider de client généré pour ajouter les en-têtes d’autorisation que votre authorizer attend. Recherchez la configuration fetch dans le <ApiName>Provider.tsx généré et ajoutez votre jeton ou clé API aux en-têtes de requête.