@softnetics/hono-react-query
v1.3.1
Published
A type-safe React Query integration for Hono applications that provides seamless data fetching, mutations, and caching with full TypeScript support.
Keywords
Readme
Type-safe React Query integration for Hono applications
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
Features
- 🔒 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
Installation
pnpm add @softnetics/hono-react-query
bun add @softnetics/hono-react-query
yarn add @softnetics/hono-react-query
npm install @softnetics/hono-react-queryPeer Dependencies
This 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 honoQuick Start
1. Define your Hono app
// 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 app2. Create the React Query client
// client.ts
import { createReactQueryClient } from '@softnetics/hono-react-query'
import type { App } from './app'
export const reactQueryClient = createReactQueryClient<App>({
baseUrl: 'http://localhost:3000',
})3. Use in your React components
// 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' } })
}API Reference
createReactQueryClient<T>(options)
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:
Query Methods
useQuery(path, method, payload, options?)
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
)queryOptions(path, method, payload, options?)
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 appMutation Methods
useMutation(path, method, options?, mutationOptions?)
Mutates 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 })
},
})mutationOptions(path, method, options?, mutationOptions?)
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 appCache Management
useGetQueryData(path, method, payload)
Gets 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' } | undefineduseSetQueryData(path, method, payload)
Manually 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)
}useInvalidateQueries(path, method, payload?, options?)
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"]useOptimisticUpdateQuery(path, method, payload)
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()
},
})Type Safety
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 payloadError Handling
The 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)
}Key Management
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 } }]Limitation
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
})License
MIT
Contributors
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
