Kết nối React với Smithy API
Generator api-connection cung cấp cách thức nhanh chóng để tích hợp trang web 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 một cách type-safe, bao gồm việc tạo client và hooks TanStack Query, hỗ trợ xác thực AWS IAM và Cognito cũng như xử lý lỗi phù hợp.
Yêu cầu tiên quyết
Phần tiêu đề “Yêu cầu 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ó:
- Một file
main.tsxđể render ứng dụng của bạn - Một backend Smithy TypeScript API đang hoạt động (được tạo bằng generator
ts#smithy-api) - Cognito Auth được thêm vào thông qua generator
ts#react-website-authnếu kết nối vớ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”- 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-connectionyarn nx g @aws/nx-plugin:api-connectionnpx nx g @aws/nx-plugin:api-connectionbunx nx g @aws/nx-plugin:api-connectionYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:api-connection --dry-runyarn nx g @aws/nx-plugin:api-connection --dry-runnpx nx g @aws/nx-plugin:api-connection --dry-runbunx nx g @aws/nx-plugin:api-connection --dry-runTùy chọn
Phần tiêu đề “Tùy chọn”| Parameter | Type | Default | Description |
|---|---|---|---|
| sourceProject Required | string | - | The source project which will call the API |
| targetProject Required | string | - | The target project containing your API |
Kết quả từ Generator
Phần tiêu đề “Kết quả từ Generator”Generator sẽ thực hiện thay đổi đối vớ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 local
- …
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 HTTP request 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 sẽ bị bỏ qua theo mặc định
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
Generator cũng sẽ thêm Runtime Config vào infrastructure của website nếu chưa có, điều này đảm bảo rằng API URL 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 Code
Phần tiêu đề “Tạo Code”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 type được tạo từ các 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 options cho TanStack Query hooks để tương tác với API của bạn bằng TanStack Query
Sử dụng Code được tạo
Phần tiêu đề “Sử dụng Code được tạo”Type-safe client được tạo 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 TanStack Query hooks, 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 bạn 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 options cần thiết cho việc 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 bao gồm hỗ trợ cho mutations sử dụng hook useMutation của TanStack Query. Điều này cung cấp cách thức rõ ràng để xử lý các thao tác create, update và delete với loading states, xử lý lỗi và optimistic updates.
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // Tạo mutation sử dụng mutation options được tạo 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 callbacks cho các mutation states khác nhau:
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // Sẽ chạy khi mutation thành công console.log('Item created:', data); // Bạn có thể điều hướng đến item mới navigate(`/items/${data.id}`); }, onError: (error) => { // Sẽ chạy khi mutation thất bại console.error('Failed to create item:', error); }, onSettled: () => { // Sẽ chạy khi mutation hoàn thành (thành công hoặc lỗi) // Nơi tốt để invalidate queries có thể bị ảnh hưởng 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); // Bạn có thể điều hướng đến item mới // 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 input, các hook được tạo cung cấp hỗ trợ cho infinite queries sử dụ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 infinite scrolling.
import { useInfiniteQuery } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemList() { const api = useMyApi(); const items = useInfiniteQuery({ ...api.listItems.infiniteQueryOptions({ limit: 10, // Số lượng items mỗi trang }, { // Đảm bảo bạn định nghĩa hàm getNextPageParam để trả về // tham số nên được truyền làm 'cursor' cho // trang tiếp theo getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* Làm phẳng mảng pages để render tất cả 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 tự động xử lý phân trang dựa trên cursor nếu API của bạn hỗ trợ. Giá trị nextCursor được trích xuất từ response và được sử dụng để fetch 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 dữ liệu ban đầu 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]);
// Hàm để load thêm 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 này bao gồm xử lý lỗi tích hợp sẵn với các typed error responses. Một type <operation-name>Error được tạo để đóng gói các error responses có thể xảy ra được định nghĩa trong Smithy model. Mỗi error 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 error 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 được typed là CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{createItem.error.error.message}</p> </div> ); case 403: // error.error được typed là CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error được typed là 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 được typed là CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{error.error.message}</p> </div> ); case 403: // error.error được typed là CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error được typed là CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}Tùy chỉnh Code được tạo
Phần tiêu đề “Tùy chỉnh Code được tạo”Một số Smithy traits được thêm vào project Smithy model mục tiêu của bạn trong extensions.smithy mà bạn có thể sử dụng để tùy chỉnh client được tạo.
Queries và Mutations
Phần tiêu đề “Queries và Mutations”Theo mặc định, các operations trong Smithy API của bạn sử dụng các HTTP methods PUT, POST, PATCH và DELETE được coi là mutations, và tất cả các operations 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 traits @query và @mutation được thêm vào model project của bạn trong extensions.smithy.
@query
Phần tiêu đề “@query”Áp dụng trait @query cho Smithy operation của bạn để buộc nó được xử lý như một query:
@http(method: "POST", uri: "/items")@queryoperation ListItems { input: ListItemsInput output: ListItemsOutput}Hook được tạo sẽ cung cấp queryOptions mặc dù nó sử dụng HTTP method 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 xử lý như một mutation:
@http(method: "GET", uri: "/start-processing")@mutationoperation StartProcessing { input: StartProcessingInput output: StartProcessingOutput}Hook được tạo sẽ cung cấp mutationOptions mặc dù nó sử dụng HTTP method GET:
const startProcessing = useMutation(api.startProcessing.mutationOptions());Custom Pagination Cursor
Phần tiêu đề “Custom Pagination Cursor”Theo mặc định, các hook được tạo 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 model project của bạn trong extensions.smithy.
Áp dụng trait @cursor với inputToken để thay đổi tên của tham số input được sử dụng cho pagination token:
@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ố input 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 := { // Tham số input tên 'cursor' sẽ khiến operation này được xử lý như operation phân trang theo mặc định cursor: String } output := { ... }}Nhóm Operations
Phần tiêu đề “Nhóm Operations”Các hook và client methods được tạo được tự động tổ chức dựa trên @tags trait trong các Smithy operations của bạn. Các operations có cùng tags được nhóm lại với nhau, giúp giữ các API calls của bạn được tổ chức và cung cấp code completion 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 sẽ được nhóm theo tags:
import { useQuery, useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() { const api = useMyApi();
// Các operations Items được nhóm dưới api.items const items = useQuery(api.items.listItems.queryOptions()); const createItem = useMutation(api.items.createItem.mutationOptions());
// Các operations Users được nhóm dưới api.users const users = useQuery(api.users.listUsers.queryOptions());
// Ví dụ sử dụng 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 hơn trong việc tổ chức các API calls của bạn và cung cấp code completion tốt hơn trong IDE của bạn.
Các operations đượ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 dữ liệu useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Các operations Items được nhóm dưới api.items const itemsData = await api.items.listItems(); setItems(itemsData);
// Các operations Users được nhóm dưới 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 { // Tạo item sử dụng phương thức được nhóm 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> );}Errors
Phần tiêu đề “Errors”Bạn có thể tùy chỉnh error responses trong Smithy API của bạn bằng cách định nghĩa các custom error structures trong Smithy model của bạn. Client được tạo sẽ tự động xử lý các custom error types này.
Định nghĩa Custom Error Structures
Phần tiêu đề “Định nghĩa Custom Error Structures”Định nghĩa các error structures 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 Errors vào Operations
Phần tiêu đề “Thêm Errors vào Operations”Chỉ định các errors mà operations 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 Custom Error Types trong React
Phần tiêu đề “Sử dụng Custom Error Types trong React”Client được tạo sẽ tự động xử lý các custom error types này, cho phép bạn type-check và xử lý các error responses khác nhau:
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// Query với xử lý lỗi được typed const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // Error được typed dựa trên các errors trong Smithy model của bạn switch (error.status) { case 404: // error.error được typed là ItemNotFoundError console.error('Not found:', error.error.message); break; case 500: // error.error được typed là InternalServerError console.error('Server error:', error.error.message); console.error('Trace ID:', error.error.traceId); break; } } });
// Mutation với xử lý lỗi được typed const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error được typed là InvalidRequestError console.error('Validation error:', error.error.message); console.error('Field errors:', error.error.fieldErrors); break; case 403: // error.error được typed là UnauthorizedError console.error('Unauthorized:', error.error.reason); break; } } });
// Component rendering với xử lý lỗi 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> {/* Nội dung component */} </div> );}Xử lý custom errors 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 với xử lý lỗi useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // Error được typed dựa trên các errors trong Smithy model của bạn const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error được typed là ItemNotFoundError console.error('Not found:', err.error.message); break; case 500: // err.error được typed là InternalServerError console.error('Server error:', err.error.message); console.error('Trace ID:', err.error.traceId); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Tạo item với xử lý lỗi const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error được typed là InvalidRequestError console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.fieldErrors); break; case 403: // err.error được typed là UnauthorizedError console.error('Unauthorized:', err.error.reason); break; } } };
// Component rendering với xử lý lỗi 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> {/* Nội dung component */} </div> );}Best Practices
Phần tiêu đề “Best Practices”Xử lý Loading States
Phần tiêu đề “Xử lý Loading States”Luôn xử lý loading và error states để 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 được typed là ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error được typed là 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ý loading states 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 được typed là ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error được typed là 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> );}Optimistic Updates
Phần tiêu đề “Optimistic Updates”Triển khai optimistic updates để 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 để fetch items const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutation để xóa items với optimistic updates const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // Hủy bất kỳ refetches đang chờ nào await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Snapshot giá trị trước đó const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Optimistically cập nhật lên giá trị mới queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Trả về context object với snapshot return { previousItems }; }, onError: (err, itemId, context) => { // Nếu mutation thất bại, sử dụng context trả về từ onMutate để roll back queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Failed to delete item:', err); }, onSettled: () => { // Luôn refetch sau error hoặc success để đảm bảo data đồng bộ với 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> );}Optimistic updates sử dụng API client trực tiếp
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Optimistically xóa item const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Khôi phục items trước đó khi có lỗi 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> );}Type Safety
Phần tiêu đề “Type Safety”Tích hợp này cung cấp type safety end-to-end hoàn chỉnh. IDE của bạn sẽ cung cấp autocompletion và type checking đầy đủ cho tất cả các API calls của bạn:
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// Type-safe mutation để tạo items const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ Type error nếu onSuccess callback không xử lý đúng response type onSuccess: (data) => { // data được typed đầy đủ dựa trên response schema của API console.log(`Item created with ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Type error nếu input không khớp với schema createItem.mutate(data); };
// Error UI có thể sử dụng type narrowing để xử lý các error types khác nhau if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error được typed là InvalidRequestError return ( <FormError message="Invalid input" errors={error.error.fieldErrors} /> ); case 403: // error.error được typed là UnauthorizedError return <AuthError reason={error.error.reason} />; default: // error.error được typed là InternalServerError cho 500, v.v. 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> );}Type safety 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 nếu input không khớp với schema await api.createItem(data); } catch (e) { // ✅ Error type bao gồm tất cả các error responses có thể const err = e as CreateItemError; switch (err.status) { case 400: // err.error được typed là InvalidRequestError console.error('Validation errors:', err.error.fieldErrors); break; case 403: // err.error được typed là UnauthorizedError console.error('Not authorized:', err.error.reason); break; case 500: // err.error được typed là InternalServerError console.error('Server error:', err.error.message); break; } setError(err); } };
// Error UI có thể sử dụng type narrowing để xử lý các error types khác nhau 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 types được tự động tạo từ OpenAPI schema 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 frontend code sau khi build.