Pular para o conteúdo

React para tRPC

Nx Plugin for AWS fornece um gerador para integrar rapidamente sua API tRPC com um site React. Ele configura toda a configuração necessária para conectar-se aos seus backends tRPC, incluindo suporte para autenticação AWS IAM e Cognito e tratamento adequado de erros. A integração fornece segurança de tipo completa de ponta a ponta entre seu frontend e backend(s) tRPC.

Antes de usar este gerador, certifique-se de que sua aplicação React tenha:

  1. Um arquivo main.tsx que renderiza sua aplicação
  2. Um elemento JSX <App/> onde o provedor tRPC será automaticamente injetado
  3. Uma API tRPC funcionando (gerada usando o gerador de API tRPC)
  4. Cognito Auth adicionado via o gerador ts#website#auth se 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>,
);
Terminal window
pnpm nx g @aws/nx-plugin:connection
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
ParâmetroTipoPadrãoDescrição
sourceProject Obrigatóriostring-O projeto de origem
targetProject Obrigatóriostring-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 booleantrueSe 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.

O gerador cria a seguinte estrutura em sua aplicação React:

  • Directorysrc
    • Directorycomponents
      • <ApiName>ClientProvider.tsx Configura os clientes tRPC e vinculações aos seus esquema(s) de backend. ApiName será resolvido para o nome da API
      • QueryClientProvider.tsx Provedor de cliente TanStack React Query
    • Directoryhooks
      • useSigV4.tsx Hook para assinar requisições HTTP com SigV4 (somente IAM)
      • use<ApiName>.tsx Um hook retornando o proxy de opções tRPC para integração com TanStack Query
      • use<ApiName>Client.tsx Um hook retornando o cliente tRPC vanilla para chamadas diretas à API

Além disso, ele instala as dependências necessárias:

  • @trpc/client
  • @trpc/tanstack-react-query
  • @tanstack/react-query
  • aws4fetch (se estiver usando autenticação IAM)
  • event-source-polyfill (se estiver usando REST API, para suporte a assinaturas)

O gerador fornece um hook use<ApiName> que retorna um proxy de opções tRPC para uso com hooks TanStack Query como useQuery e useMutation:

import { useQuery, useMutation } from '@tanstack/react-query';
import { useMyApi } from './hooks/useMyApi';
function MyComponent() {
const trpc = useMyApi();
// Example query
const { data, isLoading, error } = useQuery(trpc.users.list.queryOptions());
// Example mutation
const mutation = useMutation(trpc.users.create.mutationOptions());
const handleCreate = () => {
mutation.mutate({
name: 'John Doe',
email: 'john@example.com',
});
};
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

O hook use<ApiName>Client fornece acesso ao cliente tRPC vanilla, que é útil para chamadas imperativas à API e assinaturas:

import { useState } from 'react';
import { useMyApiClient } from './hooks/useMyApi';
function MyComponent() {
const client = useMyApiClient();
const handleClick = async () => {
const result = await client.echo.query({ message: 'Hello!' });
console.log(result);
const mutationResult = await client.users.create.mutate({ name: 'Jane' });
console.log(mutationResult);
};
return <button onClick={handleClick}>Call API</button>;
}

A integração inclui tratamento de erros integrado que processa adequadamente os erros tRPC:

function MyComponent() {
const trpc = useMyApi();
const { data, error } = useQuery(trpc.users.list.queryOptions());
if (error) {
return (
<div>
<h2>Error occurred:</h2>
<p>{error.message}</p>
{error.data?.code && <p>Code: {error.data.code}</p>}
</div>
);
}
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

Ao conectar-se a um backend tRPC REST API, o cliente gerado é automaticamente configurado com um splitLink que roteia operações de assinatura através de httpSubscriptionLink (usando SSE) e consultas/mutações regulares através de httpLink. Isso significa que as assinaturas funcionam imediatamente sem configuração adicional.

Para informações sobre como definir procedimentos de assinatura em seu backend, consulte o guia do gerador de API tRPC.

Você pode consumir assinaturas usando o hook useSubscription com subscriptionOptions do proxy de opções:

import { useSubscription } from '@trpc/tanstack-react-query';
import { useMyApi } from './hooks/useMyApi';
function StreamingComponent() {
const trpc = useMyApi();
const subscription = useSubscription(
trpc.myStream.subscriptionOptions(
{ query: 'hello' },
{
enabled: true,
onStarted: () => {
console.log('Subscription started');
},
onData: (data) => {
console.log('Received:', data.text);
},
onError: (error) => {
console.error('Subscription error:', error);
},
},
),
);
return (
<div>
<p>Status: {subscription.status}</p>
{subscription.data && <p>Latest: {subscription.data.text}</p>}
{subscription.error && <p>Error: {subscription.error.message}</p>}
<button onClick={() => subscription.reset()}>Reset</button>
</div>
);
}

O objeto subscription fornece:

  • subscription.data — os dados recebidos mais recentemente
  • subscription.error — o erro recebido mais recentemente
  • subscription.status — um de 'idle', 'connecting', 'pending', ou 'error'
  • subscription.reset() — redefine a assinatura (útil para recuperar de erros)

Alternativamente, você pode usar o cliente tRPC vanilla através do hook use<ApiName>Client para mais controle sobre o ciclo de vida da assinatura:

import { useState, useEffect } from 'react';
import { useMyApiClient } from './hooks/useMyApi';
function StreamingComponent() {
const client = useMyApiClient();
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
const subscription = client.myStream.subscribe(
{ query: 'hello' },
{
onData: (data) => {
setMessages((prev) => [...prev, data.text]);
},
onComplete: () => {
console.log('Stream complete');
},
onError: (error) => {
console.error('Stream error:', error);
},
},
);
// Clean up the subscription on unmount
return () => subscription.unsubscribe();
}, [client]);
return (
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
);
}

Sempre lide com estados de carregamento e erro para uma melhor experiência do usuário:

function UserList() {
const trpc = useMyApi();
const users = useQuery(trpc.users.list.queryOptions());
if (users.isLoading) {
return <LoadingSpinner />;
}
if (users.error) {
return <ErrorMessage error={users.error} />;
}
return (
<ul>
{users.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

Use atualizações otimistas para uma melhor experiência do usuário:

import { useQueryClient, useQuery, useMutation } from '@tanstack/react-query';
function UserList() {
const trpc = useMyApi();
const users = useQuery(trpc.users.list.queryOptions());
const queryClient = useQueryClient();
const deleteMutation = useMutation(
trpc.users.delete.mutationOptions({
onMutate: async (userId) => {
// Cancel outgoing fetches
await queryClient.cancelQueries(trpc.users.list.queryFilter());
// Get snapshot of current data
const previousUsers = queryClient.getQueryData(
trpc.users.list.queryKey(),
);
// Optimistically remove the user
queryClient.setQueryData(trpc.users.list.queryKey(), (old) =>
old?.filter((user) => user.id !== userId),
);
return { previousUsers };
},
onError: (err, userId, context) => {
// Restore previous data on error
queryClient.setQueryData(
trpc.users.list.queryKey(),
context?.previousUsers,
);
},
}),
);
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name}
<button onClick={() => deleteMutation.mutate(user.id)}>Delete</button>
</li>
))}
</ul>
);
}

Pré-busque dados para melhor desempenho:

function UserList() {
const trpc = useMyApi();
const users = useQuery(trpc.users.list.queryOptions());
const queryClient = useQueryClient();
// Prefetch user details on hover
const prefetchUser = async (userId: string) => {
await queryClient.prefetchQuery(trpc.users.getById.queryOptions(userId));
};
return (
<ul>
{users.map((user) => (
<li key={user.id} onMouseEnter={() => prefetchUser(user.id)}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
);
}

Lide com paginação com consultas infinitas:

function UserList() {
const trpc = useMyApi();
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(
trpc.users.list.infiniteQueryOptions(
{ limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
},
),
);
return (
<div>
{data?.pages.map((page) =>
page.users.map((user) => <UserCard key={user.id} user={user} />),
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}

É importante notar que consultas infinitas só podem ser usadas para procedimentos com uma propriedade de entrada chamada cursor.

A integração fornece segurança de tipo completa de ponta a ponta. Seu IDE fornecerá autocompletar completo e verificação de tipo para todas as suas chamadas de API:

function UserForm() {
const trpc = useMyApi();
// ✅ Input is fully typed
const createUser = trpc.users.create.useMutation();
const handleSubmit = (data: CreateUserInput) => {
// ✅ Type error if input doesn't match schema
createUser.mutate(data);
};
return <form onSubmit={handleSubmit}>{/* ... */}</form>;
}

Os tipos são automaticamente inferidos das definições de roteador e esquema do seu backend, garantindo que quaisquer alterações em sua API sejam imediatamente refletidas em seu código frontend sem a necessidade de compilar.

Se sua API tRPC usa autenticação Custom (Lambda Authorizer), o provedor de cliente gerado inclui headers de espaço reservado onde você deve adicionar os cabeçalhos de autorização que seu autorizador espera. Procure pelos comentários // TODO: Add headers required by your custom authorizer no <ApiName>ClientProvider.tsx gerado e substitua-os pela lógica do seu token ou chave de API.

Para mais informações, consulte: