React đến Smithy API
Generator connection cung cấp cách thức nhanh chóng để tích hợp website React của bạn với backend Smithy TypeScript API. Nó thiết lập tất cả cấu hình cần thiết để kết nối với Smithy API của bạn theo cách an toàn về kiểu, bao gồm tạo client và hook TanStack Query, hỗ trợ xác thực AWS IAM và Cognito, cùng xử lý lỗi phù hợp.
Điều kiện tiên quyết
Phần tiêu đề “Điều kiện tiên quyết”Trước khi sử dụng generator này, hãy đảm bảo ứng dụng React của bạn có:
- File
main.tsxrender ứng dụng của bạn - Backend Smithy TypeScript API đang hoạt động (được tạo bằng generator
ts#apivới--framework=smithy) - Cognito Auth được thêm qua generator
ts#website#authnếu kết nối API sử dụng xác thực Cognito hoặc IAM
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>,);Cách sử dụng
Phần tiêu đề “Cách sử dụng”Chạy Generator
Phần tiêu đề “Chạy Generator”pnpm nx g @aws/nx-plugin:connectionyarn nx g @aws/nx-plugin:connectionnpx nx g @aws/nx-plugin:connectionbunx nx g @aws/nx-plugin:connectionBạn cũng có thể thực hiện chạy thử để xem những tệp nào sẽ bị thay đổi
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- Cài đặt Nx Console VSCode Plugin nếu bạn chưa cài đặt
- Mở Nx Console trong VSCode
- Nhấp
Generate (UI)trong phần "Common Nx Commands" - Tìm kiếm
@aws/nx-plugin - connection - Điền các tham số bắt buộc
- Nhấp
Generate
Tùy chọn
Phần tiêu đề “Tùy chọn”| Tham số | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
| sourceProject Bắt buộc | string | - | Dự án nguồn |
| targetProject Bắt buộc | string | - | 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 | boolean | true | Có 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. |
Kết quả từ Generator
Phần tiêu đề “Kết quả từ Generator”Generator sẽ thực hiện thay đổi các file sau trong ứng dụng React của bạn:
Thư mụcsrc
Thư mụccomponents
- <ApiName>Provider.tsx Provider cho API client của bạn
- QueryClientProvider.tsx TanStack React Query client provider
Thư mụcRuntimeConfig/ Component cấu hình runtime cho phát triển cục bộ
- …
Thư mụchooks
- use<ApiName>.tsx Thêm hook để gọi API của bạn với state được quản lý bởi TanStack Query
- use<ApiName>Client.tsx Thêm hook để khởi tạo vanilla API client có thể gọi API của bạn.
- useSigV4.tsx Thêm hook để ký các yêu cầu HTTP với SigV4 (nếu bạn chọn xác thực IAM)
- project.json Một target mới được thêm vào build để tạo type-safe client
- .gitignore Các file client được tạo ra mặc định bị bỏ qua
Generator cũng sẽ thêm một file vào Smithy model của bạn:
Thư mụcmodel
Thư mụcsrc
- extensions.smithy Định nghĩa các trait có thể được sử dụng để tùy chỉnh client được tạo ra
Generator cũng sẽ thêm Runtime Config vào cơ sở hạ tầng website của bạn nếu chưa có, điều này đảm bảo rằng URL API cho Smithy API của bạn có sẵn trong website và được tự động cấu hình bởi hook use<ApiName>.tsx.
Tạo mã
Phần tiêu đề “Tạo mã”Tại thời điểm build, một type-safe client được tạo từ đặc tả OpenAPI của Smithy API. Điều này sẽ thêm ba file mới vào ứng dụng React của bạn:
Thư mụcsrc
Thư mụcgenerated
Thư mục<ApiName>
- types.gen.ts Các kiểu được tạo từ cấu trúc Smithy model
- client.gen.ts Type-safe client để gọi API của bạn
- options-proxy.gen.ts Cung cấp các phương thức để tạo TanStack Query hooks options cho việc tương tác với API của bạn bằng TanStack Query
Sử dụng mã được tạo
Phần tiêu đề “Sử dụng mã được tạo”Type-safe client được tạo ra có thể được sử dụng để gọi Smithy API của bạn từ ứng dụng React. Khuyến nghị sử dụng client thông qua các hook TanStack Query, nhưng bạn có thể sử dụng vanilla client nếu muốn.
Sử dụng API Hook
Phần tiêu đề “Sử dụng API Hook”Generator cung cấp hook use<ApiName> mà bạn có thể sử dụng để gọi API của mình với TanStack Query.
Queries
Phần tiêu đề “Queries”Bạn có thể sử dụng phương thức queryOptions để lấy các tùy chọn cần thiết để gọi API của bạn bằng hook useQuery của 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>;}Sử dụng API client trực tiếp
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
Phần tiêu đề “Mutations”Các hook được tạo ra bao gồm hỗ trợ cho mutations bằng hook useMutation của TanStack Query. Điều này cung cấp cách sạch sẽ để xử lý các thao tác tạo, cập nhật và xóa với trạng thái loading, xử lý lỗi và cập nhật lạc quan.
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> );}Bạn cũng có thể thêm callback cho các trạng thái mutation khác nhau:
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 sử dụng API client trực tiếp
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> );}Phân trang với Infinite Queries
Phần tiêu đề “Phân trang với Infinite Queries”Đối với các endpoint chấp nhận tham số cursor làm đầu vào, các hook được tạo ra cung cấp hỗ trợ cho infinite queries bằng hook useInfiniteQuery của TanStack Query. Điều này giúp dễ dàng triển khai chức năng “load more” hoặc cuộn vô hạn.
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> );}Các hook được tạo ra tự động xử lý phân trang dựa trên cursor nếu API của bạn hỗ trợ nó. Giá trị nextCursor được trích xuất từ response và được sử dụng để lấy trang tiếp theo.
Phân trang sử dụng API client trực tiếp
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> );}Xử lý lỗi
Phần tiêu đề “Xử lý lỗi”Tích hợp bao gồm xử lý lỗi tích hợp sẵn với các response lỗi được định kiểu. Một kiểu <operation-name>Error được tạo ra để đóng gói các response lỗi có thể xảy ra được định nghĩa trong Smithy model. Mỗi lỗi có thuộc tính status và error, và bằng cách kiểm tra giá trị của status, bạn có thể thu hẹp đến một loại lỗi cụ thể.
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>;}Xử lý lỗi sử dụng API client trực tiếp
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>;}Tùy chỉnh mã được tạo
Phần tiêu đề “Tùy chỉnh mã được tạo”Một số Smithy trait được thêm vào dự án Smithy model đích của bạn trong extensions.smithy mà bạn có thể sử dụng để tùy chỉnh client được tạo ra.
Queries và Mutations
Phần tiêu đề “Queries và Mutations”Mặc định, các operation trong Smithy API của bạn sử dụng các phương thức HTTP PUT, POST, PATCH và DELETE được coi là mutations, và tất cả các operation khác được coi là queries.
Bạn có thể thay đổi hành vi này bằng cách sử dụng các Smithy trait @query và @mutation được thêm vào dự án model của bạn trong extensions.smithy.
@query
Phần tiêu đề “@query”Sau đó áp dụng trait @query cho Smithy operation của bạn để buộc nó được coi là query:
@http(method: "POST", uri: "/items")@queryoperation ListItems { input: ListItemsInput output: ListItemsOutput}Hook được tạo ra sẽ cung cấp queryOptions ngay cả khi nó sử dụng phương thức HTTP POST:
const items = useQuery(api.listItems.queryOptions());@mutation
Phần tiêu đề “@mutation”Áp dụng trait @mutation cho Smithy operation của bạn để buộc nó được coi là mutation:
@http(method: "GET", uri: "/start-processing")@mutationoperation StartProcessing { input: StartProcessingInput output: StartProcessingOutput}Hook được tạo ra sẽ cung cấp mutationOptions ngay cả khi nó sử dụng phương thức HTTP GET:
const startProcessing = useMutation(api.startProcessing.mutationOptions());Custom Pagination Cursor
Phần tiêu đề “Custom Pagination Cursor”Mặc định, các hook được tạo ra giả định phân trang dựa trên cursor với tham số có tên cursor. Bạn có thể tùy chỉnh hành vi này bằng cách sử dụng trait @cursor được thêm vào dự án model của bạn trong extensions.smithy.
Áp dụng trait @cursor với inputToken để thay đổi tên của tham số đầu vào được sử dụng cho token phân trang:
@http(method: "GET", uri: "/items")@cursor(inputToken: "nextToken")operation ListItems { input := { nextToken: String limit: Integer } output := { items: ItemList nextToken: String }}Nếu bạn không muốn tạo infiniteQueryOptions cho một operation có tham số đầu vào có tên cursor, bạn có thể vô hiệu hóa phân trang dựa trên 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 := { ... }}Nhóm các Operation
Phần tiêu đề “Nhóm các Operation”Các hook và phương thức client được tạo ra tự động được tổ chức dựa trên @tags trait trong các Smithy operation của bạn. Các operation có cùng tag được nhóm lại với nhau, điều này giúp giữ cho các lời gọi API của bạn được tổ chức và cung cấp hoàn thành mã tốt hơn trong IDE của bạn.
Ví dụ, với Smithy model này:
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}Các hook được tạo ra sẽ được nhóm theo tag:
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> );}Việc nhóm này giúp dễ dàng tổ chức các lời gọi API của bạn và cung cấp hoàn thành mã tốt hơn trong IDE của bạn.
Các operation được nhóm sử dụng API client trực tiếp
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> );}Bạn có thể tùy chỉnh các response lỗi trong Smithy API của mình bằng cách định nghĩa các cấu trúc lỗi tùy chỉnh trong Smithy model của bạn. Client được tạo ra sẽ tự động xử lý các kiểu lỗi tùy chỉnh này.
Định nghĩa các cấu trúc lỗi tùy chỉnh
Phần tiêu đề “Định nghĩa các cấu trúc lỗi tùy chỉnh”Định nghĩa các cấu trúc lỗi của bạn trong Smithy model:
@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}Thêm lỗi vào các Operation
Phần tiêu đề “Thêm lỗi vào các Operation”Chỉ định lỗi nào mà các operation của bạn có thể trả về:
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}Sử dụng các kiểu lỗi tùy chỉnh trong React
Phần tiêu đề “Sử dụng các kiểu lỗi tùy chỉnh trong React”Client được tạo ra sẽ tự động xử lý các kiểu lỗi tùy chỉnh này, cho phép bạn kiểm tra kiểu và xử lý các response lỗi khác nhau:
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> );}Xử lý lỗi tùy chỉnh với client trực tiếp
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> );}Thực hành tốt nhất
Phần tiêu đề “Thực hành tốt nhất”Xử lý trạng thái Loading
Phần tiêu đề “Xử lý trạng thái Loading”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:
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> );}Xử lý trạng thái loading sử dụng API client trực tiếp
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> );}Cập nhật lạc quan
Phần tiêu đề “Cập nhật lạc quan”Triển khai cập nhật lạc quan để có trải nghiệm người dùng tốt hơn:
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> );}Cập nhật lạc quan sử dụng API client trực tiếp
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> );}An toàn về kiểu
Phần tiêu đề “An toàn về kiểu”Tích hợp cung cấp an toàn về kiểu hoàn toàn từ đầu đến cuối. IDE của bạn sẽ cung cấp tự động hoàn thành đầy đủ và kiểm tra kiểu cho tất cả các lời gọi API của bạn:
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> );}An toàn về kiểu sử dụng API client trực tiếp
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>;}Các kiểu được tự động tạo từ schema OpenAPI của Smithy API, đả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 trong mã frontend của bạn sau khi build.
Xác thực tùy chỉnh
Phần tiêu đề “Xác thực tùy chỉnh”Nếu Smithy API của bạn sử dụng xác thực Custom (Lambda Authorizer), bạn sẽ cần chỉnh sửa provider client được tạo ra để thêm các header ủy quyền mà authorizer của bạn mong đợi. Tìm cấu hình fetch trong <ApiName>Provider.tsx được tạo ra và thêm token hoặc API key của bạn vào các header yêu cầu.