React para API Smithy
O gerador connection fornece uma maneira de integrar rapidamente seu site React com seu backend de API TypeScript Smithy. Ele configura toda a configuração necessária para conectar-se à sua API Smithy de forma type-safe, incluindo geração de cliente e hooks TanStack Query, suporte à autenticação AWS IAM e Cognito e tratamento adequado de erros.
Pré-requisitos
Seção intitulada “Pré-requisitos”Antes de usar este gerador, certifique-se de que sua aplicação React tenha:
- Um arquivo
main.tsxque renderiza sua aplicação - Um backend de API TypeScript Smithy funcional (gerado usando o gerador
ts#apicom--framework=smithy) - Cognito Auth adicionado via gerador
ts#website#authse estiver conectando uma API que usa autenticação Cognito ou IAM
Exemplo da estrutura main.tsx necessária
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>,);Execute o Gerador
Seção intitulada “Execute o Gerador”pnpm nx g @aws/nx-plugin:connectionyarn nx g @aws/nx-plugin:connectionnpx nx g @aws/nx-plugin:connectionbunx nx g @aws/nx-plugin:connectionVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - connection - Preencha os parâmetros obrigatórios
- Clique em
Generate
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
| sourceProject Obrigatório | string | - | O projeto de origem |
| targetProject Obrigatório | string | - | O projeto de destino para conectar |
| sourceComponent | string | - | O componente de origem para conectar (nome do componente, caminho relativo à raiz do projeto de origem, ou id do gerador). Use '.' para selecionar explicitamente o projeto como origem. |
| targetComponent | string | - | O componente de destino para conectar (nome do componente, caminho relativo à raiz do projeto de destino, ou id do gerador). Use '.' para selecionar explicitamente o projeto como destino. |
| preferInstallDependencies | boolean | true | Se deve preferir instalar dependências após a execução do gerador. Defina como false para adiar a instalação ao executar múltiplos geradores em lote (uma instalação ainda é executada se necessário para que os geradores subsequentes possam calcular o grafo de projetos Nx); instale uma vez no final. |
Saída do Gerador
Seção intitulada “Saída do Gerador”O gerador fará alterações nos seguintes arquivos em sua aplicação React:
Directorysrc
Directorycomponents
- <ApiName>Provider.tsx Provider para seu cliente de API
- QueryClientProvider.tsx Provider do cliente TanStack React Query
DirectoryRuntimeConfig/ Componente de configuração de runtime para desenvolvimento local
- …
Directoryhooks
- use<ApiName>.tsx Adiciona um hook para chamar sua API com estado gerenciado pelo TanStack Query
- use<ApiName>Client.tsx Adiciona um hook para instanciar o cliente de API vanilla que pode chamar sua API.
- useSigV4.tsx Adiciona um hook para assinar requisições HTTP com SigV4 (se você selecionou autenticação IAM)
- project.json Um novo target é adicionado ao build que gera um cliente type-safe
- .gitignore Os arquivos de cliente gerados são ignorados por padrão
O gerador também adicionará um arquivo ao seu modelo Smithy:
Directorymodel
Directorysrc
- extensions.smithy Define traits que podem ser usados para customizar o cliente gerado
O gerador também adicionará Runtime Config à infraestrutura do seu site, se ainda não estiver presente, o que garante que a URL da API para sua API Smithy esteja disponível no site e automaticamente configurada pelo hook use<ApiName>.tsx.
Geração de Código
Seção intitulada “Geração de Código”No momento do build, um cliente type-safe é gerado a partir da especificação OpenAPI da sua API Smithy. Isso adicionará três novos arquivos à sua aplicação React:
Directorysrc
Directorygenerated
Directory<ApiName>
- types.gen.ts Tipos gerados a partir das estruturas do modelo Smithy
- client.gen.ts Cliente type-safe para chamar sua API
- options-proxy.gen.ts Fornece métodos para criar opções de hooks TanStack Query para interagir com sua API usando TanStack Query
Usando o Código Gerado
Seção intitulada “Usando o Código Gerado”O cliente type-safe gerado pode ser usado para chamar sua API Smithy a partir da sua aplicação React. É recomendado fazer uso do cliente através dos hooks TanStack Query, mas você pode usar o cliente vanilla se preferir.
Usando o Hook da API
Seção intitulada “Usando o Hook da API”O gerador fornece um hook use<ApiName> que você pode usar para chamar sua API com TanStack Query.
Queries
Seção intitulada “Queries”Você pode usar o método queryOptions para recuperar as opções necessárias para chamar sua API usando o hook useQuery do 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>;}Usando o cliente de API diretamente
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
Seção intitulada “Mutations”Os hooks gerados incluem suporte para mutations usando o hook useMutation do TanStack Query. Isso fornece uma maneira limpa de lidar com operações de criação, atualização e exclusão com estados de carregamento, tratamento de erros e atualizações otimistas.
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> );}Você também pode adicionar callbacks para diferentes estados 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 usando o cliente de API diretamente
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> );}Paginação com Infinite Queries
Seção intitulada “Paginação com Infinite Queries”Para endpoints que aceitam um parâmetro cursor como entrada, os hooks gerados fornecem suporte a infinite queries usando o hook useInfiniteQuery do TanStack Query. Isso facilita a implementação de funcionalidades “carregar mais” ou 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, // 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> );}Os hooks gerados lidam automaticamente com a paginação baseada em cursor se sua API suportar. O valor nextCursor é extraído da resposta e usado para buscar a próxima página.
Paginação usando o cliente da API diretamente
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> );}Tratamento de Erros
Seção intitulada “Tratamento de Erros”A integração inclui tratamento de erros com respostas tipadas. Um tipo <operation-name>Error é gerado, encapsulando as possíveis respostas de erro definidas no modelo Smithy. Cada erro possui uma propriedade status e error, e ao verificar o valor de status você pode identificar um tipo específico de erro.
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>;}Tratamento de erros usando o cliente da API diretamente
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>;}Personalizando o Código Gerado
Seção intitulada “Personalizando o Código Gerado”Uma seleção de traits Smithy são adicionadas ao projeto Smithy model em extensions.smithy que você pode usar para personalizar o cliente gerado.
Queries e Mutations
Seção intitulada “Queries e Mutations”Por padrão, operações na sua API Smithy que usam os métodos HTTP PUT, POST, PATCH e DELETE são consideradas mutations, e as demais são queries.
Você pode alterar este comportamento usando os traits @query e @mutation Smithy que são adicionados ao seu projeto de modelo em extensions.smithy.
Aplique o trait @query à sua operação Smithy para forçá-la a ser tratada como query:
@http(method: "POST", uri: "/items")@queryoperation ListItems { input: ListItemsInput output: ListItemsOutput}O hook gerado fornecerá queryOptions mesmo usando o método HTTP POST:
const items = useQuery(api.listItems.queryOptions());@mutation
Seção intitulada “@mutation”Aplique o trait @mutation à sua operação Smithy para forçá-la a ser tratada como mutation:
@http(method: "GET", uri: "/start-processing")@mutationoperation StartProcessing { input: StartProcessingInput output: StartProcessingOutput}O hook gerado fornecerá mutationOptions mesmo usando o método HTTP GET:
const startProcessing = useMutation(api.startProcessing.mutationOptions());Cursor de Paginação Personalizado
Seção intitulada “Cursor de Paginação Personalizado”Por padrão, os hooks gerados assumem paginação baseada em cursor com um parâmetro chamado cursor. Você pode personalizar este comportamento usando o trait @cursor adicionado ao seu projeto de modelo em extensions.smithy.
Aplique o trait @cursor com inputToken para alterar o nome do parâmetro de entrada usado para o token de paginação:
@http(method: "GET", uri: "/items")@cursor(inputToken: "nextToken")operation ListItems { input := { nextToken: String limit: Integer } output := { items: ItemList nextToken: String }}Se não quiser gerar infiniteQueryOptions para uma operação que tem um parâmetro de entrada chamado cursor, você pode desativar a paginação baseada em cursor:
@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 := { ... }}Agrupamento de Operações
Seção intitulada “Agrupamento de Operações”Os hooks e métodos do cliente gerado são organizados automaticamente com base no trait @tags em suas operações Smithy. Operações com as mesmas tags são agrupadas, o que ajuda a manter suas chamadas de API organizadas e fornece melhor completação de código em sua IDE.
Por exemplo, com 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}Os hooks gerados serão agrupados por 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> );}Este agrupamento facilita a organização das chamadas de API e fornece melhor completação de código em sua IDE.
Operações agrupadas usando o cliente da API diretamente
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> );}Você pode personalizar respostas de erro em sua API Smithy definindo estruturas de erro personalizadas em seu modelo Smithy. O cliente gerado lidará automaticamente com esses tipos de erro personalizados.
Definindo Estruturas de Erro Personalizadas
Seção intitulada “Definindo Estruturas de Erro Personalizadas”Defina suas estruturas de erro em seu 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}Adicionando Erros a Operações
Seção intitulada “Adicionando Erros a Operações”Especifique quais erros suas operações podem retornar:
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 Erro Personalizados em React
Seção intitulada “Usando Tipos de Erro Personalizados em React”O cliente gerado lidará automaticamente com esses tipos de erro personalizados, permitindo verificação de tipos e tratamento de diferentes respostas de erro:
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> );}Tratando erros personalizados com o cliente diretamente
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> );}Melhores Práticas
Seção intitulada “Melhores Práticas”Lidar com Estados de Carregamento
Seção intitulada “Lidar com Estados de Carregamento”Sempre trate estados de carregamento e erro para uma melhor experiência do usuário:
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> );}Lidar com estados de carregamento usando o cliente da API diretamente
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> );}Atualizações Otimistas
Seção intitulada “Atualizações Otimistas”Implemente atualizações otimistas para uma melhor experiência do usuário:
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> );}Atualizações otimistas usando o cliente da API diretamente
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> );}Segurança de Tipos
Seção intitulada “Segurança de Tipos”A integração fornece segurança de tipos completa de ponta a ponta. Sua IDE fornecerá autocompletar e verificação de tipos para todas as chamadas de 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> );}Segurança de tipos usando o cliente da API diretamente
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>;}Os tipos são gerados automaticamente a partir do schema OpenAPI da sua API Smithy, garantindo que quaisquer alterações na API sejam refletidas no código frontend após um build.
Autenticação Personalizada
Seção intitulada “Autenticação Personalizada”Se sua API Smithy usa autenticação Custom (Lambda Authorizer), você precisará editar o provedor do cliente gerado para adicionar os cabeçalhos de autorização que seu autorizador espera. Procure a configuração fetch no <ApiName>Provider.tsx gerado e adicione seu token ou chave de API aos cabeçalhos da requisição.