Queries
Fetch and subscribe to real-time data with TanStack Query.
import { useQuery } from '@tanstack/react-query';
import { useCRPC } from '@/lib/convex/crpc';
function UserProfile({ id }: { id: string }) {
const crpc = useCRPC();
const { data, isPending } = useQuery(
crpc.user.get.queryOptions({ id })
);
if (isPending) return <div>Loading...</div>;
return <div>{data?.name}</div>;
}queryOptions
The queryOptions method creates options for TanStack Query's useQuery hook.
const crpc = useCRPC();
// Basic usage
const { data } = useQuery(crpc.user.list.queryOptions({}));
// With arguments
const { data } = useQuery(crpc.user.get.queryOptions({ id: userId }));
// With TanStack Query options
const { data } = useQuery(
crpc.session.list.queryOptions(
{ userId },
{
enabled: !!userId,
placeholderData: [],
}
)
);See API Reference for the full signature and options.
With select
To transform query data with type inference, spread the query options and add select:
const { data } = useSuspenseQuery({
...crpc.http.health.queryOptions(),
select: (data) => data.status,
});
// data: string (not { status: string })TanStack Query infers the return type from the select function. Note that select cannot be passed directly to queryOptions().
Real-time Updates
By default, cRPC queries subscribe to Convex's WebSocket connection and automatically update when data changes on the server. This is the key difference from traditional REST APIs:
// This query receives real-time updates automatically
const { data } = useQuery(crpc.messages.list.queryOptions({ chatId }));
// When any client creates a message, all subscribers see it instantly
const { mutate } = useMutation(crpc.messages.create.mutationOptions());Disabling Subscriptions
For data that doesn't need real-time updates, disable subscriptions to reduce WebSocket traffic:
// One-time fetch, no subscription
const { data } = useQuery(
crpc.analytics.getReport.queryOptions(
{ period: 'monthly' },
{ subscribe: false }
)
);
// Manually refresh with invalidateQueries
const queryClient = useQueryClient();
queryClient.invalidateQueries(crpc.analytics.getReport.queryFilter());Note: With subscribe: false, the query behaves like a standard TanStack Query fetch. Use invalidateQueries to manually refresh the data when needed.
Conditional Queries
With enabled
Use enabled to conditionally run queries:
const { data: user } = useQuery(crpc.user.get.queryOptions({ id: userId }));
// Only fetch settings after user is loaded
const { data: settings } = useQuery(
crpc.user.getSettings.queryOptions(
{ userId: user?.id },
{ enabled: !!user }
)
);With skipToken
For type-safe conditional queries, pass skipToken directly to queryOptions:
import { skipToken } from '@tanstack/react-query';
const { data } = useQuery(
crpc.user.get.queryOptions(userId ? { id: userId } : skipToken)
);Auth-aware Queries
cRPC provides two ways to handle authentication in queries.
skipUnauth
For queries that require authentication but shouldn't error when logged out:
// Returns undefined instead of throwing when not authenticated
const { data } = useQuery(
crpc.user.getCurrentUser.queryOptions(
{},
{ skipUnauth: true }
)
);This is useful for:
- Optional personalization on public pages
- Prefetching user data that may or may not exist
- Avoiding auth errors during SSR/hydration
Auth metadata
Procedures with .meta({ auth: 'required' }) have two key behaviors:
- During auth loading: Query is disabled (waits for auth handshake to complete)
- After auth settles: Query is skipped if user isn't authenticated
This prevents "unauthenticated" errors on first page load - the query won't run until we know the user's auth state.
// Server-side (convex/functions/user.ts)
export const getSettings = c
.query()
.meta({ auth: 'required' }) // Won't run if not authenticated
.handler(async ({ ctx }) => {
return ctx.orm.query.user.findFirst({ where: { id: ctx.user.id } });
});
// Client-side - automatically skips when logged out
const { data } = useQuery(crpc.user.getSettings.queryOptions({}));How Auth Loading Works:
When your app loads, auth state is initially unknown (isLoading: true). How queries behave depends on their metadata:
| Procedure | Auth Loading | Logged Out |
|---|---|---|
publicQuery | Runs immediately | Runs |
optionalAuthQuery (auth: 'optional') | Waits | Runs |
authQuery (auth: 'required') | Waits | Skips |
Note: These procedure builders (authQuery, etc.) already include the correct .meta() settings. Just use authQuery - no need to add .meta({ auth: 'required' }) yourself.
When to use each:
| Use Case | Solution |
|---|---|
Query needs ctx.user | Use authQuery (has .meta({ auth: 'required' })) |
| Public page with optional user data | Use optionalAuthQuery + check ctx.user |
| Fully public data | Use publicQuery |
| Auth query that shouldn't error when logged out | Add skipUnauth: true client-side |
Loading States
Use placeholderData for skeleton UIs:
const { data, isPlaceholderData } = useQuery(
crpc.user.list.queryOptions(
{},
{
placeholderData: {
users: [
{ id: '0' as Id<'user'>, name: 'Loading...' },
{ id: '2' as Id<'user'>, name: 'Loading...' },
],
},
}
)
);
return (
<div>
{data?.users.map((user) => (
<WithSkeleton key={user.id} isLoading={isPlaceholderData}>
<UserCard user={user} />
</WithSkeleton>
))}
</div>
);Warning: Use static, predictable mock data in placeholderData. Random values cause hydration errors in SSR.
Query Keys
Get type-safe query keys for cache manipulation:
const crpc = useCRPC();
const queryClient = useQueryClient();
// Get query key
const queryKey = crpc.user.list.queryKey({});
// => ['convexQuery', 'user:list', {}]
// Read/write cache
const data = queryClient.getQueryData(queryKey);
queryClient.setQueryData(queryKey, newData);Tip: With real-time subscriptions (default), invalidateQueries is rarely needed since data updates automatically via WebSocket. Use it with subscribe: false queries for manual cache control.
Query Filters
For advanced cache operations:
// Create a filter with additional options
const filter = crpc.user.list.queryFilter(
{},
{ predicate: (query) => query.state.dataUpdatedAt > Date.now() - 60000 }
);
// Use with invalidateQueries, cancelQueries, etc.
queryClient.invalidateQueries(filter);Actions as Queries
Convex actions (which can call external APIs) can be used as one-shot queries. They don't support real-time subscriptions:
// Actions are automatically detected and don't subscribe
const { data } = useQuery(crpc.ai.analyze.queryOptions({ documentId }));Imperative Calls
For queries in event handlers, effects, or callbacks, use useCRPCClient:
import { useCRPCClient } from '@/lib/convex/crpc';
const client = useCRPCClient();
const user = await client.user.get.query({ id });
await client.user.update.mutate({ id, name: 'test' });For cache-aware fetches in render context, use queryClient.fetchQuery:
import { useQueryClient } from '@tanstack/react-query';
import { useCRPC } from '@/lib/convex/crpc';
const crpc = useCRPC();
const queryClient = useQueryClient();
const user = await queryClient.fetchQuery(crpc.user.get.queryOptions({ id }));staticQueryOptions
For prefetching in event handlers, use staticQueryOptions. Unlike queryOptions, it doesn't use hooks internally, so you can call it anywhere:
import { useQueryClient } from '@tanstack/react-query';
import { useCRPC } from '@/lib/convex/crpc';
const crpc = useCRPC();
const queryClient = useQueryClient();
// Prefetch on hover - works in event handlers
const handleMouseEnter = () => {
queryClient.prefetchQuery(crpc.user.get.staticQueryOptions({ id }));
};
// Fetch on click
const handleClick = async () => {
const user = await queryClient.fetchQuery(
crpc.user.get.staticQueryOptions({ id })
);
};Note: staticQueryOptions doesn't include reactive auth state. Auth is handled at execution time by the queryFn - if the user isn't authenticated for a protected query, it will throw an UNAUTHORIZED error.
See API Reference for a method comparison table.
Troubleshooting
"Unauthenticated" error on first page load
Symptom: Query throws UNAUTHORIZED error when page first loads, but works after refresh or navigation.
Cause: Query runs before auth handshake completes. Server receives request without token.
Fix: Ensure your server procedure uses .meta({ auth: 'required' }).
Next Steps
API Reference
queryOptions
crpc.path.to.query.queryOptions(
args, // Function arguments (or {} for no args)
options? // TanStack Query options + cRPC options
)cRPC extends TanStack Query options with:
| Option | Type | Description |
|---|---|---|
skipUnauth | boolean | Skip query when not authenticated |
subscribe | boolean | Enable real-time updates (default: true) |
Plus all standard TanStack Query options: enabled, placeholderData, select, gcTime, etc.
Method Comparison
| Method | Context | Caching | Use Case |
|---|---|---|---|
client.*.query() | Anywhere | None | Direct calls without cache |
crpc.*.queryOptions() | Render only | Uses cache | Components (uses hooks) |
crpc.*.staticQueryOptions() | Anywhere | Uses cache | Prefetching, event handlers |