Package Exports
- better-convex-query
Readme
Better Convex Query
TanStack Query-inspired React hooks for Convex with enhanced developer experience. Leverages Convex's built-in real-time sync engine - no additional caching needed!
๐ฏ Why Better Convex Query?
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 -
isLoadingvsisFetchingdistinction - โ
Smooth query transitions -
keepPreviousDatafor 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!
๐ Installation
npm install better-convex-query
# or
bun add better-convex-query๐ Usage
useQuery - TanStack Query Style
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 <div>๐ Loading...</div>;
if (isError) return <div>โ Error: {error?.message}</div>;
if (!data) return null;
return (
<div>
<h1>Status: {status}</h1>
<h2>{data.name}</h2>
<p>{data.email}</p>
</div>
);
}keepPreviousData - Smooth Pagination
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 (
<div>
{data?.projects.map(project => (
<div key={project.id}>{project.name}</div>
))}
<button
onClick={() => setPage(p => p - 1)}
disabled={page === 0}
>
Previous
</button>
<button
onClick={() => setPage(p => p + 1)}
disabled={isPlaceholderData || !data?.hasMore}
>
Next
</button>
{isFetching && <span>Loading...</span>}
</div>
);
}useMutation - Enhanced with Callbacks
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 (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit(e.target.name.value);
}}>
<input name="name" type="text" disabled={updateUser.isPending} />
<button type="submit" disabled={updateUser.isPending}>
{updateUser.isPending ? '๐พ Saving...' : '๐พ Save'}
</button>
{updateUser.error && <span>โ {updateUser.error.message}</span>}
</form>
);
}useCacheQuery - Extended Cache Lifetime
import { useCacheQuery, ConvexQueryCacheProvider } from 'better-convex-query';
import { api } from '../convex/_generated/api';
// Wrap your app
function App() {
return (
<ConvexProvider client={convex}>
<ConvexQueryCacheProvider expiration={300000}>
<YourApp />
</ConvexQueryCacheProvider>
</ConvexProvider>
);
}
// Use cached queries
function UserProfile({ userId }: { userId: string }) {
const { data } = useCacheQuery(
api.users.getUser,
{ userId }
);
return <div>{data?.name}</div>;
}๐ API Reference
useQuery
function useQuery<TQuery extends FunctionReference<'query'>>(
query: TQuery,
args: TArgs extends Record<string, never> ? 'skip' | undefined : TArgs,
options?: UseQueryOptions
): UseQueryResult<FunctionReturnType<TQuery>>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 dataerror: Error | undefined- Any error that occurredstatus: 'loading' | 'error' | 'success'- TanStack-style statusisLoading: boolean- Initial load onlyisFetching: boolean- Any load (including background refetches)isPending: boolean- Loading or error stateisSuccess: boolean- Has successful dataisError: boolean- Has errorisPlaceholderData: boolean- Whether showing previous data during transition
useMutation
function useMutation<TMutation extends FunctionReference<'mutation'>>(
mutation: TMutation,
options?: UseMutationOptions
): UseMutationResult<FunctionReturnType<TMutation>, Error, FunctionArgs<TMutation>>Options
onSuccess?: (data, variables) => void- Called on successful mutationonError?: (error, variables) => void- Called on mutation erroronSettled?: (data, error, variables) => void- Called when mutation completes
Return
mutate: (variables) => Promise<TData>- Trigger the mutationmutateAsync: (variables) => Promise<TData>- Same as mutate (alias)isPending: boolean- Whether mutation is runningerror: Error | undefined- Any error from last mutationstatus: 'idle' | 'pending' | 'error' | 'success'- Mutation statusreset: () => void- Reset error and status
๐ฏ Key Features
โ TanStack Query-Style Status System
const { status, isLoading, isFetching, isSuccess, isError } = useQuery(query, args);
// status: 'loading' | 'error' | 'success'โ Loading State Distinction
const { isLoading, isFetching } = useQuery(query, args);
// isLoading = initial load only
// isFetching = any load (initial + background refetch)โ Smooth Query Transitions (keepPreviousData)
const { data, isPlaceholderData } = useQuery(
api.projects.list,
{ page },
{ keepPreviousData: true }
);
// Shows previous data while new query loads - perfect for pagination!โ Extended Cache Lifetime (useCacheQuery)
// Keep query subscriptions alive for 5 minutes after unmount
const { data } = useCacheQuery(api.users.getUser, { userId });
// Reduces unnecessary re-fetches when navigatingโ Enhanced Mutation Callbacks
const { mutate } = useMutation(mutation, {
onSuccess: (data, variables) => { /* handle success */ },
onError: (error, variables) => { /* handle error */ },
onSettled: (data, error, variables) => { /* cleanup */ }
});โ Perfect TypeScript Inference
// 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โ Convex Compatibility
// Original Convex hooks still available
import { useConvexQuery, useConvexMutation } from 'better-convex-query';๐ง Development
# Install dependencies
bun install
# Build the library
bun run build
# Watch mode for development
bun run dev
# Run tests
bun test๐งช Testing
The library includes comprehensive tests. Run with:
bun test๐ฆ Bundle Size
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)
๐ Why This Approach?
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)
๐ License
MIT