A type-safe React Query integration for Hono applications that provides seamless data fetching, mutations, and caching with full TypeScript support.
- Features
- Installation
- Quick Start
- API Reference
- Type Safety
- Error Handling
- Key Management
- Limitation
- License
- Contributors
- Contributing
- đź”’ Type-safe: Full TypeScript support with automatic type inference from your Hono routes
- ⚡ React Query integration: Built on top of TanStack Query for powerful caching and synchronization
- 🎯 Hono-first: Designed specifically for Hono applications with automatic client generation
- 🚀 Zero configuration: Works out of the box with your existing Hono setup
- 🔄 Optimistic updates: Built-in support for optimistic UI updates
- 📦 Lightweight: Minimal bundle size with no unnecessary dependencies
pnpm add @softnetics/hono-react-query
bun add @softnetics/hono-react-query
yarn add @softnetics/hono-react-query
npm install @softnetics/hono-react-queryThis package requires the following peer dependencies:
pnpm add @tanstack/react-query hono
bun add @tanstack/react-query hono
yarn add @tanstack/react-query hono
npm install @tanstack/react-query hono// app.ts
import { Hono } from 'hono'
import { validator } from 'hono/validator'
import { z } from 'zod'
const app = new Hono()
.get('/users', (c) => {
return c.json({ users: [{ id: 'id', name: 'John Doe' }] }, 200)
})
.get('/users/:id', (c) => {
if (!c.req.param('id')) {
return c.json({ error: 'User ID is required' }, 400)
}
return c.json({ user: { id: 'id', name: 'John Doe' } }, 200)
})
.post('/users', validator('json', z.object({ name: z.string() })), async (c) => {
const body = await c.req.valid('json')
if (body.name === 'forbidden_word') {
return c.json({ error: 'Name is required' }, 400)
}
return c.json({ user: { id: 'id', name: body.name } }, 201)
})
export type AppType = typeof app// client.ts
import { createReactQueryClient } from '@softnetics/hono-react-query'
import type { App } from './app'
export const reactQueryClient = createReactQueryClient<App>({
baseUrl: 'http://localhost:3000',
})// components/UserList.tsx
import { reactQueryClient } from './client'
import { isHonoResponseError } from '@softnetics/hono-react-query'
export function UserList() {
// Type-safe query with automatic inference
const usersQuery = reactQueryClient.useQuery('/users', '$get', {})
usersQuery.data // { data: { users: { id: string; name: string }[] }, status: 200, format: 'json' } | undefined
usersQuery.error // Error | HonoResponseError<{ error: string }, 400, 'json'> | null
// Type-safe mutation with automatic inference
const createUserMutation = reactQueryClient.useMutation('/users', '$post', {
onMutate: (data) => {
return { toastId: toast.loading('Creating user...') }
},
onSuccess: (response, _, context) => {
response.data // { user: { id: string; name: string } }
response.status // 201
response.format // 'json'
toast.success('User created', { id: context.toastId })
},
onError: (error, _, context) => {
error // Error | HonoResponseError<{ error: string }, 400, 'json'>
if (isHonoResponseError(error)) {
error.data // { error: string }
error.status // 400
error.format // 'json'
}
toast.error(error.data.error, { id: context?.toastId })
},
})
createUserMutation.mutateAsync({ json: { name: 'John Doe' } })
}Creates a type-safe React Query client for your Hono application. Under the hood, it uses the hc function to create a client for your Hono application.
Parameters:
options.baseUrl- The base URL of your Hono applicationoptions- Additional Hono client options (headers, fetch options, etc.)
Returns: A client object with the following methods:
Executes a GET request with React Query caching. Under the hood, it uses the useQuery function to execute the query.
const { data, isLoading, error } = reactQueryClient.useQuery(
'/users',
'$get',
{ input: { query: { limit: 10 } } },
{ staleTime: 5 * 60 * 1000 } // 5 minutes
)Creates query options for use with useQuery or useSuspenseQuery. Under the hood, it uses the queryOptions function to create the query options.
import { reactQueryClient } from './client'
import { useQuery } from '@tanstack/react-query'
const queryOptions = reactQueryClient.queryOptions('/users', '$get', {})
const { data } = useQuery(queryOptions) // automatically inferred type based on your Hono appMutates data on the server. Under the hood, it uses the useMutation function to execute the mutation.
const createUserMutation = reactQueryClient.useMutation('/users', '$post', {
onMutate: (data) => {
return { toastId: toast.loading('Creating user...') }
},
onSuccess: (response, _, context) => {
response.data // { user: { id: string; name: string } }
response.status // 201
response.format // 'json'
toast.success('User created', { id: context.toastId })
},
onError: (error, _, context) => {
error // Error | HonoResponseError<{ error: string }, 400, 'json'>
if (isHonoResponseError(error)) {
error.data // { error: string }
error.status // 400
error.format // 'json'
}
toast.error(error?.data?.error, { id: context?.toastId })
},
})Creates mutation options for use with useMutation. Under the hood, it uses the mutationOptions function to create the mutation options.
const mutationOptions = reactQueryClient.mutationOptions('/users', '$post')
const createUserMutation = useMutation(mutationOptions) // automatically inferred type based on your Hono appGets cached type-safe query data without triggering a fetch. Under the hood, it uses the getQueryData function to get the cached data.
const getQueryData = reactQueryClient.useGetQueryData('/users', '$get', {})
const cachedData = getQueryData()
// cachedData is typed as: { data: { users: { id: string; name: string }[] }, status: 200, format: 'json' } | undefinedManually updates cached query data with a type-safe payload. Under the hood, it uses the setQueryData function to update the cached data.
const setQueryData = reactQueryClient.useSetQueryData('/users', '$get', {})
function onSubmit(newData: { users: { id: string; name: string }[] }) {
setQueryData(newData)
}Invalidates cached queries to trigger refetching with a type-safe payload. Under the hood, it uses the invalidateQueries function to invalidate the cached data.
// Exact key
const invalidateQueries = reactQueryClient.useInvalidateQueries('/users', '$get')
invalidateQueries() // Refetch all user queries
// Exact key
const invalidateQueries = reactQueryClient.useInvalidateQueries('/users/:id', '$get', {
input: { param: { id: '1' } },
})
invalidateQueries() // Invalidate the user query with the id parameter
// Partial key
const invalidateQueries = reactQueryClient.useInvalidateQueries('/users/:id', '$get')
invalidateQueries() // Invalidate all queries starting with ["/users/:id", "$get"]Performs optimistic updates with rollback capability. Under the hood, it uses the getQueryData and setQueryData functions to perform the optimistic update.
const optimisticUpdate = reactQueryClient.useOptimisticUpdateQuery('/users', '$get', {})
// combine with useMutation
const { mutate } = reactQueryClient.useMutation('/users', '$post', {
onMutate: (data) => {
const updater = optimisticUpdate((prev) => ({
...prev,
users: [...prev.users, data.json],
}))
return { updater }
},
onSuccess: (response, _, context) => {
// handle success
},
onError: (error, _, context) => {
// revert the optimistic update if the mutation fails
context?.updater?.revert()
},
})The library provides full type safety by inferring types from your Hono application:
// Your Hono app types are automatically inferred
const { data } = reactQueryClient.useQuery('/users', '$get', {})
// data is typed as: "{ data: { users: User[] }, status: number, format: 'json' } | undefined"
const mutation = reactQueryClient.useMutation('/users', '$post')
mutation.mutate({ json: { name: 'John Doe' } }) // Expect type "{ json: { name: 'John Doe' } } | undefined" for payloadThe library includes a custom error class for handling Hono responses:
import { HonoResponseError, isHonoResponseError } from '@softnetics/hono-react-query'
const { data, error } = reactQueryClient.useQuery('/users', '$get', {})
// Use "isHonoResponseError" to check if the error is a Hono response error and get the status, data, and format
if (error && isHonoResponseError(error)) {
console.log('Status:', error.status)
console.log('Data:', error.data)
console.log('Format:', error.format)
}The library automatically generates query keys based on the path, method, and payload. You can access the key generation function:
const queryOptions = reactQueryClient.queryOptions('/users', '$get', {
input: { query: { limit: 10 } },
})
const queryKey = queryOptions.queryKey // ["/users", "$get", { query: { limit: 10 } }]Users must always specify the return status from the Hono app. If not specified, the library will not be able to infer the correct type.
const app = new Hono().get('/users', (c) => {
return c.json({ users: [{ id: 'id', name: 'John Doe' }] }, 200) // "200" is required
})MIT
Contributions are welcome! Please feel free to submit a Pull Request.