JSPM

better-convex-query

0.2.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 457
  • Score
    100M100P100Q89558F
  • License MIT

TanStack Query-inspired React hooks for Convex with enhanced developer experience

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 - 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!

๐Ÿš€ 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 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

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 mutation
  • onError?: (error, variables) => void - Called on mutation error
  • onSettled?: (data, error, variables) => void - Called when mutation completes

Return

  • mutate: (variables) => Promise<TData> - Trigger the mutation
  • mutateAsync: (variables) => Promise<TData> - 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

โœ… 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