Salta ai contenuti

React a tRPC

Nx Plugin for AWS fornisce un generatore per integrare rapidamente la tua API tRPC con un sito web React. Configura tutte le impostazioni necessarie per connettersi ai tuoi backend tRPC, incluso il supporto per l’autenticazione AWS IAM e Cognito e una corretta gestione degli errori. L’integrazione fornisce una completa type safety end-to-end tra il tuo frontend e i backend tRPC.

Prima di utilizzare questo generatore, assicurati che la tua applicazione React abbia:

  1. Un file main.tsx che renderizza la tua applicazione
  2. Un elemento JSX <App/> dove il provider tRPC verrà automaticamente iniettato
  3. Un’API tRPC funzionante (generata utilizzando il generatore di API tRPC)
  4. Cognito Auth aggiunto tramite il generatore ts#website#auth se si connette un’API che utilizza l’autenticazione Cognito o IAM
Esempio della struttura richiesta per 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>,
);
Terminal window
pnpm nx g @aws/nx-plugin:connection
Puoi anche eseguire una prova per vedere quali file verrebbero modificati
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
ParametroTipoPredefinitoDescrizione
sourceProject Obbligatoriostring-Il progetto sorgente
targetProject Obbligatoriostring-Il progetto di destinazione a cui connettersi
sourceComponent string-Il componente sorgente da cui connettersi (nome del componente, percorso relativo alla radice del progetto sorgente, o id del generatore). Usare '.' per selezionare esplicitamente il progetto come sorgente.
targetComponent string-Il componente di destinazione a cui connettersi (nome del componente, percorso relativo alla radice del progetto di destinazione, o id del generatore). Usare '.' per selezionare esplicitamente il progetto come destinazione.
preferInstallDependencies booleantrueSe preferire l'installazione delle dipendenze dopo l'esecuzione del generatore. Impostare su false per rimandare l'installazione quando si eseguono più generatori in batch (l'installazione viene comunque eseguita se necessaria affinché i generatori successivi possano calcolare il grafo dei progetti Nx); installare una volta alla fine.

Il generatore crea la seguente struttura nella tua applicazione React:

  • Directorysrc
    • Directorycomponents
      • <ApiName>ClientProvider.tsx Configura i client tRPC e i binding agli schemi del tuo backend. ApiName verrà risolto con il nome dell’API
      • QueryClientProvider.tsx Provider del client TanStack React Query
    • Directoryhooks
      • useSigV4.tsx Hook per firmare le richieste HTTP con SigV4 (solo IAM)
      • use<ApiName>.tsx Un hook che restituisce il proxy delle opzioni tRPC per l’integrazione con TanStack Query
      • use<ApiName>Client.tsx Un hook che restituisce il client tRPC vanilla per chiamate API dirette

Inoltre, installa le dipendenze richieste:

  • @trpc/client
  • @trpc/tanstack-react-query
  • @tanstack/react-query
  • aws4fetch (se si utilizza l’autenticazione IAM)
  • event-source-polyfill (se si utilizza REST API, per il supporto delle subscription)

Il generatore fornisce un hook use<ApiName> che restituisce un proxy delle opzioni tRPC da utilizzare con gli hook di TanStack Query come 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>
);
}

L’hook use<ApiName>Client fornisce accesso al client tRPC vanilla, che è utile per chiamate API imperative e subscription:

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>;
}

L’integrazione include una gestione degli errori integrata che elabora correttamente gli errori 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>
);
}

Quando ci si connette a un backend tRPC REST API, il client generato è automaticamente configurato con un splitLink che instrada le operazioni di subscription attraverso httpSubscriptionLink (utilizzando SSE) e le query/mutation regolari attraverso httpLink. Ciò significa che le subscription funzionano immediatamente senza alcuna configurazione aggiuntiva.

Per informazioni su come definire le procedure di subscription nel tuo backend, consulta la guida al generatore di API tRPC.

Puoi consumare le subscription utilizzando l’hook useSubscription con subscriptionOptions dal proxy delle opzioni:

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>
);
}

L’oggetto subscription fornisce:

  • subscription.data — i dati ricevuti più di recente
  • subscription.error — l’errore ricevuto più di recente
  • subscription.status — uno tra 'idle', 'connecting', 'pending' o 'error'
  • subscription.reset() — reimposta la subscription (utile per recuperare dagli errori)

In alternativa, puoi utilizzare il client tRPC vanilla tramite l’hook use<ApiName>Client per un maggiore controllo sul ciclo di vita della subscription:

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>
);
}

Gestisci sempre gli stati di caricamento ed errore per una migliore esperienza utente:

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>
);
}

Utilizza gli aggiornamenti ottimistici per una migliore esperienza utente:

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>
);
}

Effettua il prefetch dei dati per migliori prestazioni:

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>
);
}

Gestisci la paginazione con le query infinite:

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 notare che le query infinite possono essere utilizzate solo per procedure con una proprietà di input denominata cursor.

L’integrazione fornisce una completa type safety end-to-end. Il tuo IDE fornirà il completamento automatico completo e il controllo dei tipi per tutte le tue chiamate 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>;
}

I tipi vengono automaticamente inferiti dalle definizioni del router e dello schema del tuo backend, garantendo che qualsiasi modifica alla tua API si rifletta immediatamente nel codice del frontend senza la necessità di effettuare la build.

Se la tua API tRPC utilizza l’autenticazione Custom (Lambda Authorizer), il provider del client generato include headers segnaposto dove devi aggiungere le intestazioni di autorizzazione che il tuo authorizer si aspetta. Cerca i commenti // TODO: Add headers required by your custom authorizer nel file generato <ApiName>ClientProvider.tsx e sostituiscili con la logica del tuo token o chiave API.

Per ulteriori informazioni, consulta: