Reaccionar a la API de Smithy
El generador api-connection
proporciona una forma rápida de integrar tu sitio web React con tu backend de API Smithy TypeScript. Configura toda la configuración necesaria para conectarse a tu API Smithy de manera tipada, incluyendo la generación de clientes y hooks de TanStack Query, soporte para autenticación AWS IAM y Cognito, y manejo adecuado de errores.
Requisitos previos
Sección titulada «Requisitos previos»Antes de usar este generador, asegúrate que tu aplicación React tenga:
- Un archivo
main.tsx
que renderice tu aplicación - Un backend de API Smithy TypeScript funcional (generado usando el generador
ts#smithy-api
) - Auth con Cognito añadido mediante el generador
ts#react-website-auth
si conectas una API que use autenticación Cognito o IAM
Ejemplo de la estructura requerida de 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>,);
Ejecutar el generador
Sección titulada «Ejecutar el generador»- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)
en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - api-connection
- Complete los parámetros requeridos
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:api-connection
yarn nx g @aws/nx-plugin:api-connection
npx nx g @aws/nx-plugin:api-connection
bunx nx g @aws/nx-plugin:api-connection
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:api-connection --dry-run
yarn nx g @aws/nx-plugin:api-connection --dry-run
npx nx g @aws/nx-plugin:api-connection --dry-run
bunx nx g @aws/nx-plugin:api-connection --dry-run
Opciones
Sección titulada «Opciones»Parámetro | Tipo | Predeterminado | Descripción |
---|---|---|---|
sourceProject Requerido | string | - | The source project which will call the API |
targetProject Requerido | string | - | The target project containing your API |
Salida del generador
Sección titulada «Salida del generador»El generador realizará cambios en los siguientes archivos de tu aplicación React:
Directorysrc
Directorycomponents
- <ApiName>Provider.tsx Proveedor para el cliente de tu API
- QueryClientProvider.tsx Proveedor del cliente TanStack React Query
DirectoryRuntimeConfig/ Componente de configuración en tiempo de ejecución para desarrollo local
- …
Directoryhooks
- use<ApiName>.tsx Añade un hook para llamar a tu API con estado gestionado por TanStack Query
- use<ApiName>Client.tsx Añade un hook para instanciar el cliente vanilla que puede llamar a tu API
- useSigV4.tsx Añade un hook para firmar peticiones HTTP con SigV4 (si seleccionaste autenticación IAM)
- project.json Se añade un nuevo target al build que genera un cliente tipado
- .gitignore Los archivos del cliente generado se ignoran por defecto
El generador también añadirá un archivo a tu modelo Smithy:
Directorymodel
Directorysrc
- extensions.smithy Define traits que pueden usarse para personalizar el cliente generado
Además, el generador añadirá Runtime Config a tu infraestructura de website si no existe, asegurando que la URL de la API para tu API Smithy esté disponible en el website y configurada automáticamente por el hook use<ApiName>.tsx
.
Generación de código
Sección titulada «Generación de código»En tiempo de build, se genera un cliente tipado a partir de la especificación OpenAPI de tu API Smithy. Esto añadirá tres nuevos archivos a tu aplicación React:
Directorysrc
Directorygenerated
Directory<ApiName>
- types.gen.ts Tipos generados de las estructuras del modelo Smithy
- client.gen.ts Cliente tipado para llamar a tu API
- options-proxy.gen.ts Provee métodos para crear opciones de hooks TanStack Query para interactuar con tu API
Usando el código generado
Sección titulada «Usando el código generado»El cliente tipado generado puede usarse para llamar a tu API Smithy desde tu aplicación React. Se recomienda usar los hooks de TanStack Query, pero también puedes usar el cliente vanilla si lo prefieres.
Usando el hook de la API
Sección titulada «Usando el hook de la API»El generador provee un hook use<ApiName>
que puedes usar para llamar a tu API con TanStack Query.
Consultas
Sección titulada «Consultas»Puedes usar el método queryOptions
para obtener las opciones requeridas para llamar a tu API usando el 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>;}
Usar el cliente de API directamente
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>;}
Mutaciones
Sección titulada «Mutaciones»Los hooks generados incluyen soporte para mutaciones usando el hook useMutation
de TanStack Query. Esto provee una forma limpia de manejar operaciones de creación, actualización y eliminación con estados de carga, manejo de errores y actualizaciones optimistas.
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // Crear una mutación usando las opciones generadas const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* Campos del formulario */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creando...' : 'Crear ítem'} </button>
{createItem.isSuccess && ( <div className="success"> Ítem creado con ID: {createItem.data.id} </div> )}
{createItem.isError && ( <div className="error"> Error: {createItem.error.message} </div> )} </form> );}
También puedes añadir callbacks para diferentes estados de la mutación:
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // Se ejecuta cuando la mutación tiene éxito console.log('Ítem creado:', data); // Puedes navegar al nuevo ítem navigate(`/items/${data.id}`); }, onError: (error) => { // Se ejecuta cuando la mutación falla console.error('Error al crear ítem:', error); }, onSettled: () => { // Se ejecuta cuando la mutación finaliza (éxito o error) // Buen lugar para invalidar consultas afectadas queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});
Mutaciones usando el cliente de API directamente
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); // Puedes navegar al nuevo ítem // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Error al crear ítem:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Campos del formulario */} <button type="submit" disabled={isLoading} > {isLoading ? 'Creando...' : 'Crear ítem'} </button>
{createdItem && ( <div className="success"> Ítem creado con ID: {createdItem.id} </div> )}
{error && ( <div className="error"> Error: {error.message} </div> )} </form> );}
Paginación con consultas infinitas
Sección titulada «Paginación con consultas infinitas»Para endpoints que aceptan un parámetro cursor
como entrada, los hooks generados proveen soporte para consultas infinitas usando el hook useInfiniteQuery
de TanStack Query. Esto facilita implementar funcionalidad de “cargar más” o scroll infinito.
import { useInfiniteQuery } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemList() { const api = useMyApi(); const items = useInfiniteQuery({ ...api.listItems.infiniteQueryOptions({ limit: 10, // Número de ítems por página }, { // Asegúrate de definir una función getNextPageParam para retornar // el parámetro que debe pasarse como 'cursor' para la próxima página getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* Aplanar el array de páginas para renderizar todos los ítems */} <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 ? 'Cargando más...' : items.hasNextPage ? 'Cargar más' : 'No hay más ítems'} </button> </div> );}
Los hooks generados manejan automáticamente la paginación basada en cursor si tu API lo soporta. El valor nextCursor
se extrae de la respuesta y se usa para obtener la próxima página.
Paginación usando el cliente de API directamente
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);
// Obtener datos iniciales 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]);
// Función para cargar más ítems 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 ? 'Cargando más...' : nextCursor ? 'Cargar más' : 'No hay más ítems'} </button> </div> );}
Manejo de errores
Sección titulada «Manejo de errores»La integración incluye manejo de errores integrado con respuestas de error tipadas. Se genera un tipo <operation-name>Error
que encapsula las posibles respuestas de error definidas en el modelo Smithy. Cada error tiene una propiedad status
y error
, y al verificar el valor de status
puedes restringir a un tipo específico de error.
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 está tipado como CreateItem400Response return ( <div> <h2>Entrada inválida:</h2> <p>{createItem.error.error.message}</p> </div> ); case 403: // error.error está tipado como CreateItem403Response return ( <div> <h2>No autorizado:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error está tipado como CreateItem5XXResponse return ( <div> <h2>Error del servidor:</h2> <p>{createItem.error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Crear ítem</button>;}
Manejo de errores usando el cliente de API directamente
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 está tipado como CreateItem400Response return ( <div> <h2>Entrada inválida:</h2> <p>{error.error.message}</p> </div> ); case 403: // error.error está tipado como CreateItem403Response return ( <div> <h2>No autorizado:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error está tipado como CreateItem5XXResponse return ( <div> <h2>Error del servidor:</h2> <p>{error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Crear ítem</button>;}
Personalizando el código generado
Sección titulada «Personalizando el código generado»Se añaden varios traits Smithy a tu proyecto Smithy model
en extensions.smithy
que puedes usar para personalizar el cliente generado.
Consultas y mutaciones
Sección titulada «Consultas y mutaciones»Por defecto, las operaciones en tu API Smithy que usan los métodos HTTP PUT
, POST
, PATCH
y DELETE
se consideran mutaciones, y las demás se consideran consultas.
Puedes cambiar este comportamiento usando los traits Smithy @query
y @mutation
que se añaden a tu proyecto de modelo en extensions.smithy
.
Aplica el trait @query
a tu operación Smithy para forzar que se trate como consulta:
@http(method: "POST", uri: "/items")@queryoperation ListItems { input: ListItemsInput output: ListItemsOutput}
El hook generado proveerá queryOptions
aunque use el método HTTP POST
:
const items = useQuery(api.listItems.queryOptions());
@mutation
Sección titulada «@mutation»Aplica el trait @mutation
a tu operación Smithy para forzar que se trate como mutación:
@http(method: "GET", uri: "/start-processing")@mutationoperation StartProcessing { input: StartProcessingInput output: StartProcessingOutput}
El hook generado proveerá mutationOptions
aunque use el método HTTP GET
:
const startProcessing = useMutation(api.startProcessing.mutationOptions());
Cursor de paginación personalizado
Sección titulada «Cursor de paginación personalizado»Por defecto, los hooks generados asumen paginación basada en cursor con un parámetro llamado cursor
. Puedes personalizar este comportamiento usando el trait @cursor
que se añade a tu proyecto de modelo en extensions.smithy
.
Aplica el trait @cursor
con inputToken
para cambiar el nombre del parámetro de entrada usado para el token de paginación:
@http(method: "GET", uri: "/items")@cursor(inputToken: "nextToken")operation ListItems { input := { nextToken: String limit: Integer } output := { items: ItemList nextToken: String }}
Si no quieres generar infiniteQueryOptions
para una operación que tiene un parámetro de entrada llamado cursor
, puedes deshabilitar la paginación basada en cursor:
@cursor(enabled: false)operation ListItems { input := { // El parámetro de entrada llamado 'cursor' hará que esta operación se trate como paginada por defecto cursor: String } output := { ... }}
Agrupación de operaciones
Sección titulada «Agrupación de operaciones»Los hooks y métodos del cliente generado se organizan automáticamente basados en el trait @tags
en tus operaciones Smithy. Las operaciones con los mismos tags se agrupan juntas, lo que ayuda a mantener organizadas tus llamadas API y provee mejor autocompletado en tu IDE.
Por ejemplo, con este modelo 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}
Los hooks generados se agruparán por tags:
import { useQuery, useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() { const api = useMyApi();
// Las operaciones de items se agrupan bajo api.items const items = useQuery(api.items.listItems.queryOptions()); const createItem = useMutation(api.items.createItem.mutationOptions());
// Las operaciones de users se agrupan bajo api.users const users = useQuery(api.users.listUsers.queryOptions());
// Ejemplo de uso const handleCreateItem = () => { createItem.mutate({ name: 'New Item' }); };
return ( <div> <h2>Ítems</h2> <ul> {items.data?.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Añadir ítem</button>
<h2>Usuarios</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
Esta agrupación facilita organizar tus llamadas API y provee mejor autocompletado en tu IDE.
Operaciones agrupadas usando el cliente de API directamente
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);
// Cargar datos useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Operaciones de items agrupadas bajo api.items const itemsData = await api.items.listItems(); setItems(itemsData);
// Operaciones de users agrupadas bajo api.users const usersData = await api.users.listUsers(); setUsers(usersData); } catch (error) { console.error('Error obteniendo datos:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Crear ítem usando el método agrupado const newItem = await api.items.createItem({ name: 'New Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Error creando ítem:', error); } };
if (isLoading) { return <div>Cargando...</div>; }
return ( <div> <h2>Ítems</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Añadir ítem</button>
<h2>Usuarios</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
Errores
Sección titulada «Errores»Puedes personalizar las respuestas de error en tu API Smithy definiendo estructuras de error personalizadas en tu modelo Smithy. El cliente generado manejará automáticamente estos tipos de error.
Definiendo estructuras de error personalizadas
Sección titulada «Definiendo estructuras de error personalizadas»Define tus estructuras de error en tu modelo 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}
Añadiendo errores a operaciones
Sección titulada «Añadiendo errores a operaciones»Especifica qué errores pueden retornar tus operaciones:
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}
Usando tipos de error personalizados en React
Sección titulada «Usando tipos de error personalizados en React»El cliente generado manejará automáticamente estos tipos de error, permitiéndote verificar tipos y manejar diferentes respuestas de error:
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// Consulta con manejo de errores tipado const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // El error está tipado según los errores en tu modelo Smithy switch (error.status) { case 404: // error.error está tipado como ItemNotFoundError console.error('No encontrado:', error.error.message); break; case 500: // error.error está tipado como InternalServerError console.error('Error del servidor:', error.error.message); console.error('Trace ID:', error.error.traceId); break; } } });
// Mutación con manejo de errores tipado const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error está tipado como InvalidRequestError console.error('Error de validación:', error.error.message); console.error('Errores de campo:', error.error.fieldErrors); break; case 403: // error.error está tipado como UnauthorizedError console.error('No autorizado:', error.error.reason); break; } } });
// Renderizado del componente con manejo de errores 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> {/* Contenido del componente */} </div> );}
Manejo de errores personalizados con el cliente directamente
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);
// Obtener ítem con manejo de errores useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // El error está tipado según los errores en tu modelo Smithy const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error está tipado como ItemNotFoundError console.error('No encontrado:', err.error.message); break; case 500: // err.error está tipado como InternalServerError console.error('Error del servidor:', err.error.message); console.error('Trace ID:', err.error.traceId); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Crear ítem con manejo de errores const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error está tipado como InvalidRequestError console.error('Error de validación:', err.error.message); console.error('Errores de campo:', err.error.fieldErrors); break; case 403: // err.error está tipado como UnauthorizedError console.error('No autorizado:', err.error.reason); break; } } };
// Renderizado del componente con manejo de errores 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> {/* Contenido del componente */} </div> );}
Mejores prácticas
Sección titulada «Mejores prácticas»Manejar estados de carga
Sección titulada «Manejar estados de carga»Siempre maneja estados de carga y error para una mejor experiencia de usuario:
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á tipado como ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error está tipado como ListItems5XXResponse return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="Ocurrió un error desconocido" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
Manejar estados de carga usando el cliente de API directamente
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á tipado como ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error está tipado como ListItems5XXResponse return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="Ocurrió un error desconocido" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
Actualizaciones optimistas
Sección titulada «Actualizaciones optimistas»Implementa actualizaciones optimistas para una mejor experiencia de usuario:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ItemList() { const api = useMyApi(); const queryClient = useQueryClient();
// Consulta para obtener ítems const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutación para eliminar ítems con actualizaciones optimistas const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // Cancelar cualquier refetch pendiente await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Snapshot del valor anterior const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Actualización optimista al nuevo valor queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Retornar un objeto de contexto con el snapshot return { previousItems }; }, onError: (err, itemId, context) => { // Si falla la mutación, usa el contexto de onMutate para revertir queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Error al eliminar ítem:', err); }, onSettled: () => { // Siempre refetch después de error o éxito para sincronizar datos queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="Error al cargar ítems" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Eliminando...' : 'Eliminar'} </button> </li> ))} </ul> );}
Actualizaciones optimistas usando el cliente de API directamente
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Eliminación optimista const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restaurar ítems anteriores en caso de error setItems(previousItems); console.error('Error al eliminar ítem:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Eliminar</button> </li> ))} </ul> );}
Seguridad de tipos
Sección titulada «Seguridad de tipos»La integración provee seguridad de tipos completa de extremo a extremo. Tu IDE proveerá autocompletado y verificación de tipos para todas tus llamadas API:
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// Mutación tipada para crear ítems const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ Error de tipo si onSuccess no maneja el tipo de respuesta correcto onSuccess: (data) => { // data está completamente tipado según el esquema de respuesta de tu API console.log(`Ítem creado con ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Error de tipo si la entrada no coincide con el esquema createItem.mutate(data); };
// UI de error puede usar estrechamiento de tipos para manejar diferentes errores if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error está tipado como InvalidRequestError return ( <FormError message="Entrada inválida" errors={error.error.fieldErrors} /> ); case 403: // error.error está tipado como UnauthorizedError return <AuthError reason={error.error.reason} />; default: // error.error está tipado como InternalServerError para 500, etc. return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> {/* Campos del formulario */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creando...' : 'Crear ítem'} </button> </form> );}
Seguridad de tipos usando el cliente de API directamente
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Error de tipo si la entrada no coincide con el esquema await api.createItem(data); } catch (e) { // ✅ El tipo de error incluye todas las posibles respuestas de error const err = e as CreateItemError; switch (err.status) { case 400: // err.error está tipado como InvalidRequestError console.error('Errores de validación:', err.error.fieldErrors); break; case 403: // err.error está tipado como UnauthorizedError console.error('No autorizado:', err.error.reason); break; case 500: // err.error está tipado como InternalServerError console.error('Error del servidor:', err.error.message); break; } setError(err); } };
// UI de error puede usar estrechamiento de tipos para manejar diferentes errores if (error) { switch (error.status) { case 400: return ( <FormError message="Entrada inválida" errors={error.error.fieldErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}
Los tipos se generan automáticamente desde el esquema OpenAPI de tu API Smithy, asegurando que cualquier cambio en tu API se refleje en tu código frontend después de un build.