React 连接到 FastAPI
api-connection
生成器提供了一种快速将 React 网站与 FastAPI 后端集成的解决方案。它会以类型安全的方式设置所有必要的后端连接配置,包括客户端和 TanStack Query 钩子生成、AWS IAM 和 Cognito 身份验证支持以及完善的错误处理。
使用此生成器前,请确保您的 React 应用已具备:
- 渲染应用的
main.tsx
文件 - 可运行的 FastAPI 后端(使用 FastAPI 生成器生成)
- 若需连接使用 Cognito 或 IAM 认证的 API,需通过
ts#react-website-auth
生成器 添加 Cognito 认证
所需 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>,);
- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)
在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - api-connection
- 填写必需参数
- 点击
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
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
sourceProject 必需 | string | - | The source project which will call the API |
targetProject 必需 | string | - | The target project containing your API |
生成器将对 FastAPI 项目中的以下文件进行修改:
文件夹scripts
- generate_open_api.py 添加生成 OpenAPI 规范的脚本
- project.json 在构建目标中添加调用生成脚本的任务
生成器将对 React 应用中的以下文件进行修改:
文件夹src
文件夹components
- <ApiName>Provider.tsx API 客户端 Provider 组件
- QueryClientProvider.tsx TanStack React Query 客户端 Provider
文件夹hooks
- use<ApiName>.tsx 添加通过 TanStack Query 管理 API 调用的钩子
- use<ApiName>Client.tsx 添加实例化原生 API 客户端的钩子
- useSigV4.tsx 添加支持 IAM 认证的请求签名钩子(若选择 IAM 认证)
- project.json 在构建目标中添加类型安全客户端生成任务
- .gitignore 默认忽略生成的客户端文件
生成器还会在网站基础设施中添加运行时配置(若尚未存在),确保 FastAPI 的 API URL 在网站中可用并通过 use<ApiName>.tsx
钩子自动配置。
在构建时,系统会根据 FastAPI 的 OpenAPI 规范生成类型安全的客户端,将在 React 应用中添加以下三个新文件:
文件夹src
文件夹generated
文件夹<ApiName>
- types.gen.ts 从 FastAPI 的 pydantic 模型生成的类型定义
- client.gen.ts 类型安全的 API 调用客户端
- options-proxy.gen.ts 提供创建 TanStack Query 钩子选项的方法
使用生成代码
Section titled “使用生成代码”推荐通过 TanStack Query 钩子使用生成的类型安全客户端调用 FastAPI,也可以直接使用原生客户端。
使用 API 钩子
Section titled “使用 API 钩子”生成器提供的 use<ApiName>
钩子可用于通过 TanStack Query 调用 API。
使用 queryOptions
方法获取调用 API 所需的选项参数:
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>;}
直接使用原生客户端
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>;}
生成的钩子支持使用 TanStack Query 的 useMutation
处理创建、更新和删除操作,提供加载状态管理、错误处理和乐观更新功能。
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // 使用生成的变更选项创建 mutation const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* 表单字段 */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? '创建中...' : '创建条目'} </button>
{createItem.isSuccess && ( <div className="success"> 创建成功,ID: {createItem.data.id} </div> )}
{createItem.isError && ( <div className="error"> 错误: {createItem.error.message} </div> )} </form> );}
可为不同变更状态添加回调函数:
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // 变更成功时执行 console.log('条目已创建:', data); // 可导航至新条目 navigate(`/items/${data.id}`); }, onError: (error) => { // 变更失败时执行 console.error('创建失败:', error); }, onSettled: () => { // 变更完成时执行(无论成功与否) // 适合在此处刷新相关查询 queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});
直接使用客户端进行变更
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: '新条目', description: '新建条目' }); setCreatedItem(newItem); // 可导航至新条目 // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('创建失败:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* 表单字段 */} <button type="submit" disabled={isLoading} > {isLoading ? '创建中...' : '创建条目'} </button>
{createdItem && ( <div className="success"> 创建成功,ID: {createdItem.id} </div> )}
{error && ( <div className="error"> 错误: {error.message} </div> )} </form> );}
无限滚动分页
Section titled “无限滚动分页”对于支持 cursor
参数的分页端点,生成的钩子提供基于 TanStack Query useInfiniteQuery
的无限滚动分页支持。
import { useInfiniteQuery } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemList() { const api = useMyApi(); const items = useInfiniteQuery({ ...api.listItems.infiniteQueryOptions({ limit: 10, // 每页条目数 }, { // 定义获取下一页参数的函数 getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* 展开所有页面的条目进行渲染 */} <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 ? '加载更多...' : items.hasNextPage ? '加载更多' : '已无更多内容'} </button> </div> );}
若 API 支持游标分页,生成的钩子会自动处理分页逻辑,从响应中提取 nextCursor
用于获取下一页。
直接使用客户端实现分页
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);
// 初始数据获取 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]);
// 加载更多函数 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 ? '加载中...' : nextCursor ? '加载更多' : '已无更多内容'} </button> </div> );}
集成方案包含类型化的错误响应处理,生成 <operation-name>Error
类型封装 OpenAPI 规范定义的可能错误响应。通过检查 status
属性值可精确定位具体错误类型。
import { useMutation } from '@tanstack/react-query';
function MyComponent() { const api = useMyApi(); const createItem = useMutation(api.createItem.mutationOptions());
const handleClick = () => { createItem.mutate({ name: '新条目' }); };
if (createItem.error) { switch (createItem.error.status) { case 400: // error.error 类型为 CreateItem400Response return ( <div> <h2>无效输入:</h2> <p>{createItem.error.error.message}</p> <ul> {createItem.error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: // error.error 类型为 CreateItem403Response return ( <div> <h2>未授权:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error 类型为 CreateItem5XXResponse return ( <div> <h2>服务器错误:</h2> <p>{createItem.error.error.message}</p> <p>追踪 ID: {createItem.error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>创建条目</button>;}
直接使用客户端的错误处理
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: '新条目' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: // error.error 类型为 CreateItem400Response return ( <div> <h2>无效输入:</h2> <p>{error.error.message}</p> <ul> {error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: // error.error 类型为 CreateItem403Response return ( <div> <h2>未授权:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error 类型为 CreateItem5XXResponse return ( <div> <h2>服务器错误:</h2> <p>{error.error.message}</p> <p>追踪 ID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>创建条目</button>;}
流式响应处理
Section titled “流式响应处理”若已为 FastAPI 配置流式响应,useQuery
钩子会在新数据块到达时自动更新。
示例:
function MyStreamingComponent() { const api = useMyApi(); const stream = useQuery(api.myStream.queryOptions());
return ( <ul> {(stream.data ?? []).map((chunk) => ( <li> {chunk.timestamp.toISOString()}: {chunk.message} </li> ))} </ul> );}
可通过 isLoading
和 fetchStatus
属性判断流状态,生命周期如下:
-
发送流式 HTTP 请求
isLoading
为true
fetchStatus
为'fetching'
data
为undefined
-
接收首块数据
isLoading
变为false
fetchStatus
保持'fetching'
data
变为包含首块数据的数组
-
接收后续数据块
isLoading
保持false
fetchStatus
保持'fetching'
data
随新数据块实时更新
-
流式传输完成
isLoading
保持false
fetchStatus
变为'idle'
data
包含所有接收的数据块
直接使用客户端的流式处理
若已为 FastAPI 配置流式响应,生成的客户端将包含使用 for await
语法异步迭代流数据块的方法。
示例:
function MyStreamingComponent() { const api = useMyApiClient();
const [chunks, setChunks] = useState<Chunk[]>([]);
useEffect(() => { const streamChunks = async () => { for await (const chunk of api.myStream()) { setChunks((prev) => [...prev, chunk]); } }; streamChunks(); }, [api]);
return ( <ul> {chunks.map((chunk) => ( <li> {chunk.timestamp.toISOString()}: {chunk.message} </li> ))} </ul> );}
自定义生成代码
Section titled “自定义生成代码”查询与变更操作
Section titled “查询与变更操作”默认将 FastAPI 中 PUT
、POST
、PATCH
和 DELETE
方法视为变更操作,其他视为查询操作。可通过 x-query
和 x-mutation
扩展修改此行为。
x-query
Section titled “x-query”@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...
生成的钩子将为 POST 方法提供 queryOptions
:
const items = useQuery(api.listItems.queryOptions());
x-mutation
Section titled “x-mutation”@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...
生成的钩子将为 GET 方法提供 mutationOptions
:
const startProcessing = useMutation(api.startProcessing.mutationOptions());
自定义分页游标
Section titled “自定义分页游标”默认假设分页参数名为 cursor
,可通过 x-cursor
扩展自定义:
@app.get( "/items", openapi_extra={ # 指定不同的游标参数名 "x-cursor": "page_token" })def list_items(page_token: str = None, limit: int = 10): # ... return { "items": items, "page_token": next_page_token }
若需禁用某端点的无限查询选项,可将 x-cursor
设为 False
:
@app.get( "/items", openapi_extra={ # 禁用该端点的游标分页 "x-cursor": False })def list_items(page: int = 1, limit: int = 10): # ... return { "items": items, "total": total_count, "page": page, "pages": total_pages }
生成的钩子和客户端方法根据 FastAPI 端点的 OpenAPI 标签自动组织,便于管理相关 API 调用。
示例:
@app.get( "/items", tags=["items"],)def list(): # ...
@app.post( "/items", tags=["items"],)def create(item: Item): # ...
@app.get( "/users", tags=["users"],)def list(): # ...
生成的钩子将按标签分组:
import { useQuery, useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() { const api = useMyApi();
// items 操作分组在 api.items 下 const items = useQuery(api.items.list.queryOptions()); const createItem = useMutation(api.items.create.mutationOptions());
// users 操作分组在 api.users 下 const users = useQuery(api.users.list.queryOptions());
// 使用示例 const handleCreateItem = () => { createItem.mutate({ name: '新条目' }); };
return ( <div> <h2>条目</h2> <ul> {items.data?.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>添加条目</button>
<h2>用户</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
此分组机制便于组织 API 调用,并提升 IDE 的代码补全体验。
直接使用分组客户端
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);
// 数据加载 useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// items 操作分组在 api.items 下 const itemsData = await api.items.list(); setItems(itemsData);
// users 操作分组在 api.users 下 const usersData = await api.users.list(); setUsers(usersData); } catch (error) { console.error('数据获取失败:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // 使用分组方法创建条目 const newItem = await api.items.create({ name: '新条目' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('创建失败:', error); } };
if (isLoading) { return <div>加载中...</div>; }
return ( <div> <h2>条目</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>添加条目</button>
<h2>用户</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
通过定义自定义异常类、异常处理程序和响应模型,可定制 FastAPI 的错误响应。生成的客户端会自动处理这些自定义错误类型。
定义错误模型
Section titled “定义错误模型”使用 Pydantic 定义错误模型:
from pydantic import BaseModel
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]
创建自定义异常
Section titled “创建自定义异常”为不同场景创建异常类:
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = details
添加异常处理程序
Section titled “添加异常处理程序”注册异常处理程序将异常转换为 HTTP 响应:
from fastapi import Requestfrom fastapi.responses import JSONResponse
@app.exception_handler(NotFoundException)async def not_found_handler(request: Request, exc: NotFoundException): return JSONResponse( status_code=404, content=exc.message, )
@app.exception_handler(ValidationException)async def validation_error_handler(request: Request, exc: ValidationException): return JSONResponse( status_code=400, content=exc.details.model_dump(), )
指定响应模型
Section titled “指定响应模型”在端点定义中指定不同状态码的响应模型:
@app.get( "/items/{item_id}", responses={ 404: {"model": str} 500: {"model": ErrorDetails} })def get_item(item_id: str) -> Item: item = find_item(item_id) if not item: raise NotFoundException(message=f"未找到 ID 为 {item_id} 的条目") return item
@app.post( "/items", responses={ 400: {"model": ValidationError}, 403: {"model": str} })def create_item(item: Item) -> Item: if not is_valid(item): raise ValidationException( ValidationError( message="无效条目数据", field_errors=["名称不能为空"] ) ) return save_item(item)
在 React 中使用自定义错误类型
Section titled “在 React 中使用自定义错误类型”生成的客户端会自动处理这些自定义错误类型,支持类型检查和错误响应处理:
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// 带类型错误处理的查询 const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // 根据 FastAPI 响应定义错误类型 switch (error.status) { case 404: // error.error 类型为字符串 console.error('未找到:', error.error); break; case 500: // error.error 类型为 ErrorDetails console.error('服务器错误:', error.error.message); break; } } });
// 带类型错误处理的变更 const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error 类型为 ValidationError console.error('验证错误:', error.error.message); console.error('字段错误:', error.error.field_errors); break; case 403: // error.error 类型为字符串 console.error('禁止访问:', error.error); break; } } });
// 带错误处理的组件渲染 if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error} />; } else { return <ErrorMessage message={getItem.error.error.message} />; } }
return ( <div> {/* 组件内容 */} </div> );}
直接处理自定义错误
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);
// 带错误处理的数据获取 useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // 根据 FastAPI 响应定义错误类型 const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error 类型为字符串 console.error('未找到:', err.error); break; case 500: // err.error 类型为 ErrorDetails console.error('服务器错误:', err.error.message); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// 带错误处理的条目创建 const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error 类型为 ValidationError console.error('验证错误:', err.error.message); console.error('字段错误:', err.error.field_errors); break; case 403: // err.error 类型为字符串 console.error('禁止访问:', err.error); break; } } };
// 带错误处理的组件渲染 if (loading) { return <LoadingSpinner />; }
if (error) { if (error.status === 404) { return <NotFoundMessage message={error.error} />; } else if (error.status === 500) { return <ErrorMessage message={error.error.message} />; } }
return ( <div> {/* 组件内容 */} </div> );}
处理加载状态
Section titled “处理加载状态”始终处理加载和错误状态以提升用户体验:
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 类型为 ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error 类型为 ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`追踪 ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="未知错误" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
直接处理加载状态
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 类型为 ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error 类型为 ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`追踪 ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="未知错误" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
实现乐观更新以提升用户体验:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ItemList() { const api = useMyApi(); const queryClient = useQueryClient();
// 获取条目的查询 const itemsQuery = useQuery(api.listItems.queryOptions());
// 带乐观更新的删除操作 const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // 取消正在进行的查询 await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// 保存当前数据快照 const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// 乐观更新数据 queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// 返回包含快照的上下文 return { previousItems }; }, onError: (err, itemId, context) => { // 操作失败时回滚数据 queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('删除失败:', err); }, onSettled: () => { // 操作完成后刷新数据 queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="加载失败" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? '删除中...' : '删除'} </button> </li> ))} </ul> );}
直接实现乐观更新
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // 乐观删除条目 const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // 出错时恢复数据 setItems(previousItems); console.error('删除失败:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>删除</button> </li> ))} </ul> );}
集成方案提供完整的端到端类型安全,IDE 将为所有 API 调用提供自动补全和类型检查:
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// 类型安全的条目创建操作 const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ 若 onSuccess 回调未正确处理响应类型会报错 onSuccess: (data) => { // data 类型基于 API 响应模式 console.log(`创建成功,ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ 若输入不符合模式会报错 createItem.mutate(data); };
// 错误 UI 可使用类型细化处理不同错误 if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error 类型为 CreateItem400Response return ( <FormError message="无效输入" errors={error.error.validationErrors} /> ); case 403: // error.error 类型为 CreateItem403Response return <AuthError reason={error.error.reason} />; default: // error.error 类型为 CreateItem5XXResponse return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: '新条目' }); }}> {/* 表单字段 */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? '创建中...' : '创建条目'} </button> </form> );}
直接使用客户端的类型安全
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ 输入不符合模式会报错 await api.createItem(data); } catch (e) { // ✅ 错误类型包含所有可能的错误响应 const err = e as CreateItemError; switch (err.status) { case 400: // err.error 类型为 CreateItem400Response console.error('验证错误:', err.error.validationErrors); break; case 403: // err.error 类型为 CreateItem403Response console.error('未授权:', err.error.reason); break; case 500: case 502: // err.error 类型为 CreateItem5XXResponse console.error( '服务器错误:', err.error.message, '追踪 ID:', err.error.traceId, ); break; } setError(err); } };
// 错误 UI 可使用类型细化处理不同错误 if (error) { switch (error.status) { case 400: return ( <FormError message="无效输入" errors={error.error.validationErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}
所有类型均从 FastAPI 的 OpenAPI 规范自动生成,确保 API 变更后前端代码在构建后自动同步更新。