Skip to content

A type-safe React Query integration for Hono applications that provides seamless data fetching, mutations, and caching with full TypeScript support.

License

softnetics/hono-react-query

Repository files navigation

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

  • đź”’ 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-query

Peer 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 hono

Quick 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 app

2. 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 application
  • options - 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 app

Mutation 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 app

Cache 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' } | undefined

useSetQueryData(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 payload

Error 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.

About

A type-safe React Query integration for Hono applications that provides seamless data fetching, mutations, and caching with full TypeScript support.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •