23.02-React集成Hono API
要点
- React 集成 Hono API 的核心是 Hono RPC 客户端(hc)
- RPC 客户端提供完整的类型推导,后端改动前端立即感知
- 配合 React Query 可以轻松处理缓存、加载状态、错误处理
- Server Component 和 Client Component 都可以使用 RPC 客户端
内容
1. Hono RPC 客户端基础
Hono 的 RPC 客户端(hc)是前后端类型安全的关键。它可以从后端 app 类型自动推导出前端调用的类型。
1.1 后端导出 AppType
// apps/api/src/index.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
// 定义路由(必须链式调用)
app
.get('/api/users/:id', async (c) => {
const id = c.req.param('id')
return c.json({
id,
name: 'Alice',
email: '[email protected]',
})
})
.post(
'/api/chat',
zValidator('json', z.object({
message: z.string(),
})),
async (c) => {
const { message } = c.req.valid('json')
return c.json({ reply: `你说了:${message}` })
}
)
// 导出类型
export type AppType = typeof app
export default app注意:路由必须用链式调用(.get().post()),不能用 app.get() 分开写,否则类型推导会失败。
1.2 前端创建 RPC 客户端
// apps/web/lib/api.ts
import { hc } from 'hono/client'
import type { AppType } from '@api/src/index'
export const api = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!)
// 现在 api 有完整的类型提示
// api.api.users[':id'].$get()
// api.api.chat.$post()2. 在 React 组件中使用
2.1 Server Component
Server Component 可以直接 await RPC 调用:
// apps/web/app/users/[id]/page.tsx
import { api } from '@/lib/api'
export default async function UserPage({ params }: { params: { id: string } }) {
const res = await api.api.users[':id'].$get({
param: { id: params.id },
})
if (!res.ok) {
return <div>用户不存在</div>
}
const user = await res.json()
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}Server Component 的优点:
- 数据在服务端获取,SEO 友好
- 不需要
useEffect、useState - 自动处理加载和错误状态
2.2 Client Component
Client Component 需要用 useEffect 或 React Query:
// apps/web/components/UserProfile.tsx
'use client'
import { useState, useEffect } from 'react'
import { api } from '@/lib/api'
export function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
async function fetchUser() {
try {
const res = await api.api.users[':id'].$get({
param: { id: userId },
})
if (!res.ok) {
throw new Error('Failed to fetch')
}
const data = await res.json()
setUser(data)
} catch (err) {
setError(err)
} finally {
setLoading(false)
}
}
fetchUser()
}, [userId])
if (loading) return <div>加载中...</div>
if (error) return <div>加载失败</div>
if (!user) return <div>用户不存在</div>
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}这种方式需要手动处理加载、错误状态,代码比较冗长。推荐使用 React Query。
3. 配合 React Query
React Query 是 React 生态中最流行的数据获取库,和 Hono RPC 完美配合。
3.1 安装和配置
npm install @tanstack/react-query// apps/web/app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 分钟内认为数据是新鲜的
retry: 1,
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}// apps/web/app/layout.tsx
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}3.2 查询数据
// apps/web/components/UserProfile.tsx
'use client'
import { useQuery } from '@tanstack/react-query'
import { api } from '@/lib/api'
export function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const res = await api.api.users[':id'].$get({
param: { id: userId },
})
if (!res.ok) {
throw new Error('Failed to fetch')
}
return res.json()
},
})
if (isLoading) return <div>加载中...</div>
if (error) return <div>加载失败</div>
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
)
}React Query 自动处理:
- 缓存:相同 queryKey 的请求不会重复发送
- 重新获取:窗口聚焦、网络恢复时自动刷新
- 加载状态:
isLoading、isFetching - 错误处理:
error对象
3.3 变更数据
// apps/web/components/ChatForm.tsx
'use client'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '@/lib/api'
export function ChatForm() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: async (message: string) => {
const res = await api.api.chat.$post({
json: { message },
})
if (!res.ok) {
throw new Error('Failed to send')
}
return res.json()
},
onSuccess: (data) => {
// 成功后刷新聊天列表
queryClient.invalidateQueries({ queryKey: ['chat-history'] })
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.target)
const message = formData.get('message') as string
mutation.mutate(message)
}}
>
<input name="message" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '发送中...' : '发送'}
</button>
{mutation.data && <p>{mutation.data.reply}</p>}
{mutation.error && <p>发送失败</p>}
</form>
)
}4. 自定义 Hook 封装
为了复用和简化,可以封装自定义 Hook:
// apps/web/hooks/use-user.ts
import { useQuery } from '@tanstack/react-query'
import { api } from '@/lib/api'
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const res = await api.api.users[':id'].$get({
param: { id: userId },
})
if (!res.ok) {
throw new Error('Failed to fetch')
}
return res.json()
},
})
}// 使用自定义 Hook
import { useUser } from '@/hooks/use-user'
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = useUser(userId)
if (isLoading) return <div>加载中...</div>
if (error) return <div>加载失败</div>
return <h1>{user.name}</h1>
}5. 类型推导工具
Hono 提供了类型推导工具,可以从 RPC 方法中提取请求和响应类型:
// apps/web/lib/api-types.ts
import type { InferResponseType, InferRequestType } from 'hono/client'
import { api } from './api'
// 响应类型
export type UserResponse = InferResponseType<typeof api.api.users[':id'].$get>
// 请求参数类型
export type ChatRequestArgs = InferRequestType<typeof api.api.chat.$post>
// 使用
function processUser(user: UserResponse) {
console.log(user.name)
}
function sendChat(args: ChatRequestArgs) {
api.api.chat.$post(args)
}6. 错误处理
6.1 统一错误处理
// apps/web/lib/api-client.ts
import { hc } from 'hono/client'
import type { AppType } from '@api/src/index'
class ApiError extends Error {
constructor(
public status: number,
public message: string,
public data?: any
) {
super(message)
}
}
async function handleResponse<T>(res: Response): Promise<T> {
if (!res.ok) {
const error = await res.json().catch(() => null)
throw new ApiError(
res.status,
error?.message || `Request failed with status ${res.status}`,
error
)
}
return res.json()
}
export const apiClient = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!, {
fetch: async (input, init) => {
const res = await fetch(input, init)
return res
},
})// 使用
try {
const res = await apiClient.api.users[':id'].$get({ param: { id: '123' } })
const user = await handleResponse(res)
} catch (err) {
if (err instanceof ApiError) {
if (err.status === 404) {
console.log('用户不存在')
} else if (err.status === 401) {
console.log('未登录')
}
}
}6.2 React Query 全局错误处理
// apps/web/app/providers.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// 401、403、404 不重试
if (error.status === 401 || error.status === 403 || error.status === 404) {
return false
}
return failureCount < 3
},
},
mutations: {
retry: false,
},
},
})7. 请求拦截器
添加认证 token、日志等:
// apps/web/lib/api.ts
import { hc } from 'hono/client'
import type { AppType } from '@api/src/index'
export const api = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!, {
headers: {
'Content-Type': 'application/json',
},
fetch: async (input, init) => {
// 添加认证 token
const token = localStorage.getItem('token')
const headers = new Headers(init?.headers)
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
// 发送请求
const res = await fetch(input, { ...init, headers })
// 处理 401
if (res.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return res
},
})8. 实战:完整的 React + Hono 集成
// apps/web/lib/api.ts
import { hc } from 'hono/client'
import type { AppType } from '@api/src/index'
export const api = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!)
// 自定义 Hook
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const res = await api.api.users[':id'].$get({
param: { id: userId },
})
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
},
})
}
export function useCreateChat() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (message: string) => {
const res = await api.api.chat.$post({
json: { message },
})
if (!res.ok) throw new Error('Failed to send')
return res.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['chat-history'] })
},
})
}// apps/web/components/Chat.tsx
'use client'
import { useUser, useCreateChat } from '@/lib/api'
export function Chat({ userId }: { userId: string }) {
const { data: user, isLoading } = useUser(userId)
const createChat = useCreateChat()
if (isLoading) return <div>加载中...</div>
return (
<div>
<h1>和 {user.name} 聊天</h1>
<form
onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.target)
const message = formData.get('message') as string
createChat.mutate(message)
e.target.reset()
}}
>
<input name="message" />
<button type="submit" disabled={createChat.isPending}>
发送
</button>
</form>
{createChat.data && <p>{createChat.data.reply}</p>}
</div>
)
}9. 小结
React 集成 Hono API 的关键点:
- RPC 客户端:使用
hc创建带类型的客户端 - Server Component:直接
awaitRPC 调用,代码简洁 - Client Component:配合 React Query 处理缓存、加载、错误状态
- 自定义 Hook:封装常用查询,提高复用性
- 类型推导:使用
InferResponseType等工具提取类型 - 错误处理:统一处理 API 错误,避免重复代码
一句话带走:
Hono RPC 客户端 + React Query = 类型安全、缓存自动管理、加载状态处理的完整解决方案。