
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
better-convex-query
Advanced tools
TanStack Query-inspired React hooks for Convex with enhanced developer experience
TanStack 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:
status: 'loading' | 'error' | 'success'isLoading vs isFetching distinctionkeepPreviousData for flicker-free paginationonSuccess, onError, onSettlednpm install better-convex-query
# or
bun add better-convex-query
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>
);
}
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>
);
}
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>
);
}
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>;
}
function useQuery<TQuery extends FunctionReference<'query'>>(
query: TQuery,
args: TArgs extends Record<string, never> ? 'skip' | undefined : TArgs,
options?: UseQueryOptions
): UseQueryResult<FunctionReturnType<TQuery>>
enabled?: boolean - Whether to fetch data (default: true)keepPreviousData?: boolean - Show previous data while new query loads (default: false)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 transitionfunction useMutation<TMutation extends FunctionReference<'mutation'>>(
mutation: TMutation,
options?: UseMutationOptions
): UseMutationResult<FunctionReturnType<TMutation>, Error, FunctionArgs<TMutation>>
onSuccess?: (data, variables) => void - Called on successful mutationonError?: (error, variables) => void - Called on mutation erroronSettled?: (data, error, variables) => void - Called when mutation completesmutate: (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 statusconst { status, isLoading, isFetching, isSuccess, isError } = useQuery(query, args);
// status: 'loading' | 'error' | 'success'
const { isLoading, isFetching } = useQuery(query, args);
// isLoading = initial load only
// isFetching = any load (initial + background refetch)
const { data, isPlaceholderData } = useQuery(
api.projects.list,
{ page },
{ keepPreviousData: true }
);
// Shows previous data while new query loads - perfect for pagination!
// Keep query subscriptions alive for 5 minutes after unmount
const { data } = useCacheQuery(api.users.getUser, { userId });
// Reduces unnecessary re-fetches when navigating
const { mutate } = useMutation(mutation, {
onSuccess: (data, variables) => { /* handle success */ },
onError: (error, variables) => { /* handle error */ },
onSettled: (data, error, variables) => { /* cleanup */ }
});
// 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
// Original Convex hooks still available
import { useConvexQuery, useConvexMutation } from 'better-convex-query';
# Install dependencies
bun install
# Build the library
bun run build
# Watch mode for development
bun run dev
# Run tests
bun test
The library includes comprehensive tests. Run with:
bun test
Since bundle size doesn't matter for this library, we prioritize:
Convex already provides:
We add:
We don't add:
MIT
FAQs
TanStack Query-inspired React hooks for Convex with enhanced developer experience
The npm package better-convex-query receives a total of 460 weekly downloads. As such, better-convex-query popularity was classified as not popular.
We found that better-convex-query demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.