React to tRPC
AWS Plugin for Nx provides a generator to quickly integrate your tRPC API with a React website. It sets up all necessary configuration for connecting to your tRPC backends, including AWS IAM and Cognito authentication support and proper error handling. The integration provides full end-to-end type safety between your frontend and tRPC backend(s).
Prerequisites
Section titled “Prerequisites”Before using this generator, ensure your React application has:
- A
main.tsx
file that renders your application - An
<App/>
JSX element where the tRPC provider will be automatically injected - A working tRPC API (generated using the tRPC API generator)
- Cognito Auth added via the
ts#cloudscape-website-auth
generator if connecting an API which uses Cognito or IAM auth
Example of required main.tsx
structure
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>,);
Run the Generator
Section titled “Run the Generator”- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)
in the "Common Nx Commands" section - Search for
@aws/nx-plugin - api-connection
- Fill in the required parameters
- Click
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
You can also perform a dry-run to see what files would be changed
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
Options
Section titled “Options”Parameter | Type | Default | Description |
---|---|---|---|
sourceProject Required | string | - | The source project which will call the API |
targetProject Required | string | - | The target project containing your API |
Generator Output
Section titled “Generator Output”The generator creates the following structure in your React application:
Directorysrc
Directorycomponents
- <ApiName>ClientProvider.tsx Sets up the tRPC clients and bindings to your backend schema(s). ApiName will resolve to the name of the API
- QueryClientProvider.tsx TanStack React Query client provider
Directoryhooks
- useSigV4.tsx Hook for signing HTTP requests with SigV4 (IAM only)
- use<ApiName>.tsx A hook for the given backend API.
Additionally, it installs the required dependencies:
@trpc/client
@trpc/tanstack-react-query
@tanstack/react-query
aws4fetch
(if using IAM auth)
Using the Generated Code
Section titled “Using the Generated Code”Using the tRPC Hook
Section titled “Using the tRPC Hook”The generator provides a use<ApiName>
hook that gives you access to the type-safe tRPC client:
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> );}
Error Handling
Section titled “Error Handling”The integration includes built-in error handling that properly processes tRPC errors:
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> );}
Best Practices
Section titled “Best Practices”Handle Loading States
Section titled “Handle Loading States”Always handle loading and error states for a better user experience:
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> );}
Optimistic Updates
Section titled “Optimistic Updates”Use optimistic updates for a better user experience:
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> );}
Prefetching Data
Section titled “Prefetching Data”Prefetch data for better performance:
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> );}
Infinite Queries
Section titled “Infinite Queries”Handle pagination with infinite queries:
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> );}
It is important to note that infinite queries can only be used for procedures with an input property named cursor
.
Type Safety
Section titled “Type Safety”The integration provides complete end-to-end type safety. Your IDE will provide full autocompletion and type checking for all your API calls:
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>;}
The types are automatically inferred from your backend’s router and schema definitions, ensuring that any changes to your API are immediately reflected in your frontend code without the need to build.
More Information
Section titled “More Information”For more information, please refer to the tRPC TanStack React Query documentation.
You can also refer directly to the TanStack Query documentation.