Bỏ qua để đến nội dung

React đến tRPC

Nx Plugin for AWS cung cấp một generator để nhanh chóng tích hợp API tRPC của bạn với một trang web React. Nó thiết lập tất cả cấu hình cần thiết để kết nối với các backend tRPC của bạn, bao gồm hỗ trợ xác thực AWS IAM và Cognito cũng như xử lý lỗi phù hợp. Tích hợp này cung cấp type safety đầy đủ từ đầu đến cuối giữa frontend và (các) backend tRPC của bạn.

Trước khi sử dụng generator này, hãy đảm bảo ứng dụng React của bạn có:

  1. Một file main.tsx render ứng dụng của bạn
  2. Một phần tử JSX <App/> nơi tRPC provider sẽ được tự động inject
  3. Một API tRPC hoạt động (được tạo bằng tRPC API generator)
  4. Cognito Auth được thêm qua ts#website#auth generator nếu kết nối một API sử dụng Cognito hoặc IAM auth
Ví dụ về cấu trúc main.tsx bắt buộc
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
Bạn cũng có thể thực hiện chạy thử để xem những tệp nào sẽ bị thay đổi
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
Tham sốKiểuMặc địnhMô tả
sourceProject Bắt buộcstring-Dự án nguồn
targetProject Bắt buộcstring-Dự án đích để kết nối tới
sourceComponent string-Component nguồn để kết nối từ đó (tên component, đường dẫn tương đối so với thư mục gốc của dự án nguồn, hoặc generator id). Sử dụng '.' để chọn rõ ràng dự án làm nguồn.
targetComponent string-Component đích để kết nối tới (tên component, đường dẫn tương đối so với thư mục gốc của dự án đích, hoặc generator id). Sử dụng '.' để chọn rõ ràng dự án làm đích.
preferInstallDependencies booleantrueCó nên cài đặt các dependencies sau khi generator chạy hay không. Đặt thành false để trì hoãn việc cài đặt khi chạy nhiều generator liên tiếp (việc cài đặt vẫn sẽ chạy nếu cần thiết để các generator tiếp theo có thể tính toán Nx project graph); cài đặt một lần vào cuối.

Generator tạo ra cấu trúc sau trong ứng dụng React của bạn:

  • Thư mụcsrc
    • Thư mụccomponents
      • <ApiName>ClientProvider.tsx Thiết lập các tRPC client và binding đến (các) schema backend của bạn. ApiName sẽ được giải quyết thành tên của API
      • QueryClientProvider.tsx TanStack React Query client provider
    • Thư mụchooks
      • useSigV4.tsx Hook để ký các HTTP request với SigV4 (chỉ IAM)
      • use<ApiName>.tsx Một hook trả về tRPC options proxy cho tích hợp TanStack Query
      • use<ApiName>Client.tsx Một hook trả về vanilla tRPC client cho các lời gọi API trực tiếp

Ngoài ra, nó cài đặt các dependency cần thiết:

  • @trpc/client
  • @trpc/tanstack-react-query
  • @tanstack/react-query
  • aws4fetch (nếu sử dụng IAM auth)
  • event-source-polyfill (nếu sử dụng REST API, để hỗ trợ subscription)

Generator cung cấp một hook use<ApiName> trả về một tRPC options proxy để sử dụng với các hook TanStack Query như useQueryuseMutation:

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

Hook use<ApiName>Client cung cấp quyền truy cập vào vanilla tRPC client, hữu ích cho các lời gọi API mệnh lệnh và 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>;
}

Tích hợp này bao gồm xử lý lỗi tích hợp sẵn để xử lý đúng các lỗi 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>
);
}

Khi kết nối với một backend tRPC REST API, client được tạo tự động được cấu hình với một splitLink định tuyến các thao tác subscription thông qua httpSubscriptionLink (sử dụng SSE) và các query/mutation thông thường thông qua httpLink. Điều này có nghĩa là subscription hoạt động ngay lập tức mà không cần cấu hình bổ sung.

Để biết thông tin về cách định nghĩa các procedure subscription trong backend của bạn, xem hướng dẫn tRPC API generator.

Bạn có thể sử dụng subscription bằng hook useSubscription với subscriptionOptions từ options proxy:

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

Đối tượng subscription cung cấp:

  • subscription.data — dữ liệu nhận được gần đây nhất
  • subscription.error — lỗi nhận được gần đây nhất
  • subscription.status — một trong 'idle', 'connecting', 'pending', hoặc 'error'
  • subscription.reset() — reset subscription (hữu ích để khôi phục từ lỗi)

Ngoài ra, bạn có thể sử dụng vanilla tRPC client qua hook use<ApiName>Client để kiểm soát nhiều hơn vòng đời 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>
);
}

Luôn xử lý trạng thái loading và lỗi để có trải nghiệm người dùng tốt hơn:

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

Sử dụng optimistic update để có trải nghiệm người dùng tốt hơn:

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

Prefetch dữ liệu để có hiệu suất tốt hơn:

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

Xử lý phân trang với infinite query:

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

Điều quan trọng cần lưu ý là infinite query chỉ có thể được sử dụng cho các procedure có thuộc tính input có tên cursor.

Tích hợp này cung cấp type safety hoàn chỉnh từ đầu đến cuối. IDE của bạn sẽ cung cấp autocompletion và type checking đầy đủ cho tất cả các lời gọi API của bạn:

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

Các type được tự động suy ra từ các định nghĩa router và schema của backend của bạn, đảm bảo rằng bất kỳ thay đổi nào đối với API của bạn đều được phản ánh ngay lập tức trong code frontend của bạn mà không cần build.

Nếu API tRPC của bạn sử dụng xác thực Custom (Lambda Authorizer), client provider được tạo bao gồm placeholder headers nơi bạn phải thêm các authorization header mà authorizer của bạn mong đợi. Tìm các comment // TODO: Add headers required by your custom authorizer trong file <ApiName>ClientProvider.tsx được tạo và thay thế chúng bằng logic token hoặc API key của bạn.

Để biết thêm thông tin, vui lòng tham khảo: