Réagir à l'API Smithy
Le générateur connection permet d’intégrer rapidement votre site React avec votre backend d’API Smithy TypeScript. Il configure tous les éléments nécessaires pour se connecter à votre API Smithy de manière typée, incluant la génération de client et de hooks TanStack Query, la prise en charge de l’authentification AWS IAM et Cognito, ainsi qu’une gestion d’erreurs appropriée.
Prérequis
Section intitulée « Prérequis »Avant d’utiliser ce générateur, assurez-vous que votre application React dispose :
- D’un fichier
main.tsxqui rend votre application - D’un backend d’API Smithy TypeScript fonctionnel (généré via le générateur
ts#smithy-api) - D’une authentification Cognito ajoutée via le générateur
ts#react-website-authsi vous connectez une API utilisant l’authentification Cognito ou IAM
Exemple de structure requise pour 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>,);Utilisation
Section intitulée « Utilisation »Exécuter le générateur
Section intitulée « Exécuter le générateur »- 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
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| Paramètre | Type | Par défaut | Description |
|---|---|---|---|
| sourceProject Requis | string | - | The source project |
| targetProject Requis | string | - | The target project to connect to |
Résultat du générateur
Section intitulée « Résultat du générateur »Le générateur modifiera les fichiers suivants dans votre application React :
Répertoiresrc
Répertoirecomponents
- <ApiName>Provider.tsx Provider pour le client de votre API
- QueryClientProvider.tsx Provider du client React Query de TanStack
RépertoireRuntimeConfig/ Composant de configuration runtime pour le développement local
- …
Répertoirehooks
- use<ApiName>.tsx Ajoute un hook pour appeler votre API avec état géré par TanStack Query
- use<ApiName>Client.tsx Ajoute un hook pour instancier le client vanilla pouvant 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 à la compilation pour générer un client typé
- .gitignore Les fichiers clients 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 des traits pour personnaliser le client généré
Le générateur ajoutera aussi une Configuration Runtime à votre infrastructure de site si elle n’existe pas déjà, garantissant que l’URL de votre API Smithy est disponible dans le site et configurée automatiquement par le hook use<ApiName>.tsx.
Génération de code
Section intitulée « Génération de code »À la compilation, un client typé 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 typé pour appeler votre API
- options-proxy.gen.ts Fournit des méthodes pour créer des options de hooks TanStack Query interagissant avec votre API
Utiliser le code généré
Section intitulée « Utiliser le code généré »Le client typé généré peut être utilisé pour appeler votre API Smithy depuis votre application React. Il est recommandé d’utiliser les hooks TanStack Query, mais le client vanilla est aussi disponible.
Utiliser le hook d’API
Section intitulée « Utiliser le hook d’API »Le générateur fournit un hook use<ApiName> pour appeler votre API avec TanStack Query.
Requêtes
Section intitulée « Requêtes »Utilisez la méthode queryOptions pour récupérer les options nécessaires à l’appel de votre API via 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>Chargement...</div>; if (item.isError) return <div>Erreur : {item.error.message}</div>;
return <div>Élément : {item.data.name}</div>;}Utiliser le client d'API directement
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>Chargement...</div>; if (error) return <div>Erreur : {error.message}</div>;
return <div>Élément : {item.name}</div>;}Mutations
Section intitulée « Mutations »Les hooks générés prennent en charge les mutations via le hook useMutation de TanStack Query. Cela permet de gérer les opérations de création, mise à jour et suppression avec états de chargement, gestion d’erreurs et mises à jour optimistes.
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // Crée une mutation avec les options générées const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'Nouvel élément', description: 'Un nouvel élément' }); };
return ( <form onSubmit={handleSubmit}> {/* Champs du formulaire */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Création...' : 'Créer l\'élément'} </button>
{createItem.isSuccess && ( <div className="success"> Élément créé avec l'ID : {createItem.data.id} </div> )}
{createItem.isError && ( <div className="error"> Erreur : {createItem.error.message} </div> )} </form> );}Vous pouvez aussi ajouter des callbacks pour différents états de mutation :
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // Ceci s'exécutera lorsque la mutation réussit console.log('Élément créé :', data); // Vous pouvez naviguer vers le nouvel élément navigate(`/items/${data.id}`); }, onError: (error) => { // Ceci s'exécutera lorsque la mutation échoue console.error('Échec de la création :', error); }, onSettled: () => { // Ceci s'exécutera lorsque la mutation se termine (succès ou erreur) // Bon endroit pour invalider les requêtes qui pourraient être affectées queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});Mutations avec le client d'API directement
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: 'Nouvel élément', description: 'Un nouvel élément' }); setCreatedItem(newItem); // Vous pouvez naviguer vers le nouvel élément // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Échec de la création :', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Champs du formulaire */} <button type="submit" disabled={isLoading} > {isLoading ? 'Création...' : 'Créer l\'élément'} </button>
{createdItem && ( <div className="success"> Élément créé avec l'ID : {createdItem.id} </div> )}
{error && ( <div className="error"> Erreur : {error.message} </div> )} </form> );}Pagination avec requêtes infinies
Section intitulée « Pagination avec requêtes infinies »Pour les endpoints acceptant un paramètre cursor, les hooks générés prennent en charge les requêtes infinies via useInfiniteQuery de TanStack Query, facilitant l’implémentation de fonctionnalités “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, // Nombre d'éléments par page }, { // Assurez-vous de définir une fonction getNextPageParam pour retourner // le paramètre qui doit être passé comme 'cursor' pour la // page suivante getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* Aplatir le tableau des pages pour afficher tous les éléments */} <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 ? 'Chargement...' : items.hasNextPage ? 'Charger plus' : 'Fin des éléments'} </button> </div> );}Les hooks générés gèrent automatiquement la pagination par curseur si votre API la supporte. La valeur nextCursor est extraite de la réponse pour récupérer la page suivante.
Pagination avec le client d'API directement
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);
// Récupérer les données initiales 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]);
// Fonction pour charger plus d'éléments 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 ? 'Chargement...' : nextCursor ? 'Charger plus' : 'Fin des éléments'} </button> </div> );}Gestion des erreurs
Section intitulée « Gestion des erreurs »L’intégration inclut une gestion d’erreurs typée. Un type <operation-name>Error est généré, encapsulant les réponses d’erreur possibles définies dans le modèle Smithy. Chaque erreur a une propriété status et error, permettant de cibler 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: 'Nouvel élément' }); };
if (createItem.error) { switch (createItem.error.status) { case 400: // error.error est typé comme CreateItem400Response return ( <div> <h2>Entrée invalide :</h2> <p>{createItem.error.error.message}</p> </div> ); case 403: // error.error est typé comme CreateItem403Response return ( <div> <h2>Non autorisé :</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error est typé comme CreateItem5XXResponse return ( <div> <h2>Erreur serveur :</h2> <p>{createItem.error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Créer l'élément</button>;}Gestion des erreurs avec le client d'API directement
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'Nouvel élément' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: // error.error est typé comme CreateItem400Response return ( <div> <h2>Entrée invalide :</h2> <p>{error.error.message}</p> </div> ); case 403: // error.error est typé comme CreateItem403Response return ( <div> <h2>Non autorisé :</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error est typé comme CreateItem5XXResponse return ( <div> <h2>Erreur serveur :</h2> <p>{error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Créer l'élément</button>;}Personnalisation du code généré
Section intitulée « Personnalisation du code généré »Des traits Smithy sont ajoutés à votre projet Smithy dans extensions.smithy pour personnaliser le client généré.
Requêtes et mutations
Section intitulée « Requêtes et mutations »Par défaut, les opérations utilisant les méthodes HTTP PUT, POST, PATCH et DELETE sont considérées comme des mutations, et toutes les autres comme des requêtes.
Vous pouvez modifier ce comportement avec les traits Smithy @query et @mutation ajoutés à votre projet de modèle dans extensions.smithy.
Appliquez le trait @query à votre opération Smithy pour forcer son traitement en 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 forcer son traitement en 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 par curseur avec un paramètre nommé cursor. Vous pouvez personnaliser ce comportement avec le trait @cursor ajouté à votre projet de modèle dans extensions.smithy.
Appliquez le trait @cursor avec inputToken pour changer 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 ayant un paramètre d’entrée nommé cursor, vous pouvez désactiver la pagination par curseur :
@cursor(enabled: false)operation ListItems { input := { // Un paramètre d'entrée nommé 'cursor' fera que cette opération sera traitée comme paginée par défaut cursor: String } output := { ... }}Regroupement d’opérations
Section intitulée « Regroupement d’opérations »Les hooks et méthodes client générés sont automatiquement organisés selon le trait @tags de vos opérations Smithy. Les opérations partageant les mêmes tags sont regroupées, ce qui aide à organiser vos appels d’API et améliore l’autocomplétion 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();
// Les opérations Items sont regroupées sous api.items const items = useQuery(api.items.listItems.queryOptions()); const createItem = useMutation(api.items.createItem.mutationOptions());
// Les opérations Users sont regroupées sous api.users const users = useQuery(api.users.listUsers.queryOptions());
// Exemple d'utilisation const handleCreateItem = () => { createItem.mutate({ name: 'Nouvel élément' }); };
return ( <div> <h2>Éléments</h2> <ul> {items.data?.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Ajouter un élément</button>
<h2>Utilisateurs</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 améliore l’autocomplétion dans votre IDE.
Opérations regroupées avec le client d'API directement
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);
// Charger les données useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Les opérations Items sont regroupées sous api.items const itemsData = await api.items.listItems(); setItems(itemsData);
// Les opérations Users sont regroupées sous api.users const usersData = await api.users.listUsers(); setUsers(usersData); } catch (error) { console.error('Erreur lors de la récupération des données :', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Créer un élément en utilisant la méthode regroupée const newItem = await api.items.createItem({ name: 'Nouvel élément' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Erreur lors de la création de l\'élément :', error); } };
if (isLoading) { return <div>Chargement...</div>; }
return ( <div> <h2>Éléments</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Ajouter un élément</button>
<h2>Utilisateurs</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éfinir des structures d’erreur personnalisées
Section intitulée « Définir des 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}Ajouter des erreurs aux opérations
Section intitulée « Ajouter des 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}Utiliser les types d’erreur personnalisés dans React
Section intitulée « Utiliser les 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 les types et gérer différentes réponses d’erreur :
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// Requête avec gestion d'erreur typée const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // L'erreur est typée selon les erreurs de votre modèle Smithy switch (error.status) { case 404: // error.error est typé comme ItemNotFoundError console.error('Non trouvé :', error.error.message); break; case 500: // error.error est typé comme InternalServerError console.error('Erreur serveur :', error.error.message); console.error('ID de trace :', error.error.traceId); break; } } });
// Mutation avec gestion d'erreur typée const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error est typé comme InvalidRequestError console.error('Erreur de validation :', error.error.message); console.error('Erreurs de champs :', error.error.fieldErrors); break; case 403: // error.error est typé comme UnauthorizedError console.error('Non autorisé :', error.error.reason); break; } } });
// Rendu du composant avec gestion d'erreur 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> {/* Contenu du composant */} </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);
// Récupérer l'élément avec gestion d'erreur useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // L'erreur est typée selon les erreurs de votre modèle Smithy const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error est typé comme ItemNotFoundError console.error('Non trouvé :', err.error.message); break; case 500: // err.error est typé comme InternalServerError console.error('Erreur serveur :', err.error.message); console.error('ID de trace :', err.error.traceId); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Créer un élément avec gestion d'erreur const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error est typé comme InvalidRequestError console.error('Erreur de validation :', err.error.message); console.error('Erreurs de champs :', err.error.fieldErrors); break; case 403: // err.error est typé comme UnauthorizedError console.error('Non autorisé :', err.error.reason); break; } } };
// Rendu du composant avec gestion d'erreur 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> {/* Contenu du composant */} </div> );}Bonnes pratiques
Section intitulée « Bonnes pratiques »Gérer les états de chargement
Section intitulée « Gérer les états de chargement »Toujours gérer 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 est typé comme ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error est typé comme ListItems5XXResponse return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="Une erreur inconnue s'est produite" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}Gérer les états de chargement avec le client d'API directement
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 est typé comme ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error est typé comme ListItems5XXResponse return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="Une erreur inconnue s'est produite" />; } }
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();
// Requête pour récupérer les éléments const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutation pour supprimer des éléments avec mises à jour optimistes const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // Annuler toutes les récupérations en cours await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Capturer la valeur précédente const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Mettre à jour de façon optimiste vers la nouvelle valeur queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Retourner un objet de contexte avec la capture return { previousItems }; }, onError: (err, itemId, context) => { // Si la mutation échoue, utiliser le contexte retourné par onMutate pour annuler queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Échec de la suppression de l\'élément :', err); }, onSettled: () => { // Toujours récupérer après erreur ou succès pour s'assurer que les données sont synchronisées avec le serveur queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="Échec du chargement des éléments" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Suppression...' : 'Supprimer'} </button> </li> ))} </ul> );}Mises à jour optimistes avec le client d'API directement
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Supprimer l'élément de façon optimiste const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restaurer les éléments précédents en cas d'erreur setItems(previousItems); console.error('Échec de la suppression de l\'élément :', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Supprimer</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 l’autocomplétion et la vérification de type pour tous vos appels d’API :
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// Mutation typée pour créer des éléments const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ Erreur de type si le callback onSuccess ne gère pas le bon type de réponse onSuccess: (data) => { // data est entièrement typé selon le schéma de réponse de votre API console.log(`Élément créé avec l'ID : ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Erreur de type si l'entrée ne correspond pas au schéma createItem.mutate(data); };
// L'interface d'erreur peut utiliser le rétrécissement de type pour gérer différents types d'erreur if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error est typé comme InvalidRequestError return ( <FormError message="Entrée invalide" errors={error.error.fieldErrors} /> ); case 403: // error.error est typé comme UnauthorizedError return <AuthError reason={error.error.reason} />; default: // error.error est typé comme InternalServerError pour 500, etc. return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'Nouvel élément' }); }}> {/* Champs du formulaire */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Création...' : 'Créer l\'élément'} </button> </form> );}Sécurité des types avec le client d'API directement
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Erreur de type si l'entrée ne correspond pas au schéma await api.createItem(data); } catch (e) { // ✅ Le type d'erreur inclut toutes les réponses d'erreur possibles const err = e as CreateItemError; switch (err.status) { case 400: // err.error est typé comme InvalidRequestError console.error('Erreurs de validation :', err.error.fieldErrors); break; case 403: // err.error est typé comme UnauthorizedError console.error('Non autorisé :', err.error.reason); break; case 500: // err.error est typé comme InternalServerError console.error('Erreur serveur :', err.error.message); break; } setError(err); } };
// L'interface d'erreur peut utiliser le rétrécissement de type pour gérer différents types d'erreur if (error) { switch (error.status) { case 400: return ( <FormError message="Entrée invalide" 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 compilation.