TanStack Query-inspired React hooks for Convex with enhanced developer experience
npm install better-convex-queryTanStack Query-inspired React hooks for Convex with enhanced developer experience. Leverages Convex's built-in real-time sync engine - no additional caching needed!
Convex already handles all the complex stuff (caching, retry logic, real-time subscriptions), but the basic useQuery hook lacks the developer experience of TanStack Query. This library provides:
- โ
Full TanStack Query-style status system - status: 'loading' | 'error' | 'success'
- โ
Enhanced loading states - isLoading vs isFetching distinction
- โ
Smooth query transitions - keepPreviousData for flicker-free pagination
- โ
Query caching support - Optional cache provider for extended subscription lifetimes
- โ
Mutation callbacks - onSuccess, onError, onSettled
- โ
Advanced TypeScript inference - Perfect type safety
- โ
Zero additional complexity - Convex handles the hard stuff!
``bash`
npm install better-convex-queryor
bun add better-convex-query
`tsx
import { useQuery } from 'better-convex-query';
import { api } from '../convex/_generated/api';
function UserProfile({ userId }: { userId: string }) {
const {
data,
error,
status,
isLoading,
isFetching,
isPending,
isSuccess,
isError
} = useQuery(
api.users.getUser,
{ userId },
{ enabled: !!userId }
);
if (isLoading) return
return (
{data.email}
$3
`tsx
import { useQuery } from 'better-convex-query';
import { api } from '../convex/_generated/api';function ProjectsList() {
const [page, setPage] = useState(0);
const { data, isPlaceholderData, isFetching } = useQuery(
api.projects.list,
{ page },
{ keepPreviousData: true }
);
return (
{data?.projects.map(project => (
{project.name}
))}
onClick={() => setPage(p => p - 1)}
disabled={page === 0}
>
Previous
onClick={() => setPage(p => p + 1)}
disabled={isPlaceholderData || !data?.hasMore}
>
Next
{isFetching && Loading...}
);
}
`$3
`tsx
import { useMutation } from 'better-convex-query';
import { api } from '../convex/_generated/api';function UpdateUserForm({ userId }: { userId: string }) {
const updateUser = useMutation(
api.users.updateUser,
{
onSuccess: (data, variables) => {
console.log('โ
User updated!', data);
},
onError: (error, variables) => {
console.error('โ Update failed:', error);
},
onSettled: (data, error, variables) => {
console.log('๐ Update completed');
}
}
);
const handleSubmit = async (name: string) => {
try {
await updateUser.mutate({ userId, name });
} catch (error) {
// Error already handled in onError callback
}
};
return (
);
}
`$3
`tsx
import { useCacheQuery, ConvexQueryCacheProvider } from 'better-convex-query';
import { api } from '../convex/_generated/api';// Wrap your app
function App() {
return (
);
}
// Use cached queries
function UserProfile({ userId }: { userId: string }) {
const { data } = useCacheQuery(
api.users.getUser,
{ userId }
);
return
{data?.name};
}
`๐ API Reference
$3
`typescript
function useQuery>(
query: TQuery,
args: TArgs extends Record ? 'skip' | undefined : TArgs,
options?: UseQueryOptions
): UseQueryResult>
`#### Options
-
enabled?: boolean - Whether to fetch data (default: true)
- keepPreviousData?: boolean - Show previous data while new query loads (default: false)#### Return
-
data: TData | undefined - The query result data
- error: Error | undefined - Any error that occurred
- status: 'loading' | 'error' | 'success' - TanStack-style status
- isLoading: boolean - Initial load only
- isFetching: boolean - Any load (including background refetches)
- isPending: boolean - Loading or error state
- isSuccess: boolean - Has successful data
- isError: boolean - Has error
- isPlaceholderData: boolean - Whether showing previous data during transition$3
`typescript
function useMutation>(
mutation: TMutation,
options?: UseMutationOptions
): UseMutationResult, Error, FunctionArgs>
`#### Options
-
onSuccess?: (data, variables) => void - Called on successful mutation
- onError?: (error, variables) => void - Called on mutation error
- onSettled?: (data, error, variables) => void - Called when mutation completes#### Return
-
mutate: (variables) => Promise - Trigger the mutation
- mutateAsync: (variables) => Promise - Same as mutate (alias)
- isPending: boolean - Whether mutation is running
- error: Error | undefined - Any error from last mutation
- status: 'idle' | 'pending' | 'error' | 'success' - Mutation status
- reset: () => void - Reset error and status๐ฏ Key Features
$3
`typescript
const { status, isLoading, isFetching, isSuccess, isError } = useQuery(query, args);
// status: 'loading' | 'error' | 'success'
`$3
`typescript
const { isLoading, isFetching } = useQuery(query, args);
// isLoading = initial load only
// isFetching = any load (initial + background refetch)
`$3
`typescript
const { data, isPlaceholderData } = useQuery(
api.projects.list,
{ page },
{ keepPreviousData: true }
);
// Shows previous data while new query loads - perfect for pagination!
`$3
`typescript
// Keep query subscriptions alive for 5 minutes after unmount
const { data } = useCacheQuery(api.users.getUser, { userId });
// Reduces unnecessary re-fetches when navigating
`$3
`typescript
const { mutate } = useMutation(mutation, {
onSuccess: (data, variables) => { / handle success / },
onError: (error, variables) => { / handle error / },
onSettled: (data, error, variables) => { / cleanup / }
});
`$3
`typescript
// Types are automatically inferred from your Convex functions
const { data } = useQuery(api.users.getUser, { userId: '123' });
// data is automatically typed as the return type of api.users.getUser
`$3
`typescript
// Original Convex hooks still available
import { useConvexQuery, useConvexMutation } from 'better-convex-query';
`๐ง Development
`bash
Install dependencies
bun installBuild the library
bun run buildWatch mode for development
bun run devRun tests
bun test
`๐งช Testing
The library includes comprehensive tests. Run with:
`bash
bun test
``Since bundle size doesn't matter for this library, we prioritize:
- โ
Perfect TypeScript inference
- โ
Comprehensive error handling
- โ
Full feature parity with TanStack Query patterns
- โ
Zero runtime overhead (just wrappers around Convex)
Convex already provides:
- โ
Real-time subscriptions
- โ
Automatic caching
- โ
Retry logic
- โ
Optimistic updates
- โ
Connection management
We add:
- โ
Better developer experience (TanStack-style API)
- โ
Enhanced loading states
- โ
Mutation callbacks
- โ
Perfect TypeScript support
We don't add:
- โ Additional caching (Convex handles this)
- โ Retry logic (Convex handles this)
- โ Complex state management (Convex handles this)
- โ Bundle bloat (just thin wrappers)
MIT