Patterns
Loading and error states
useQuery returns undefined while loading and on error. Errors are thrown as React errors and can be caught by an error boundary:
import { useQuery } from '@pbvex/react';
import { ErrorBoundary } from 'react-error-boundary';
function MessageList({ channel }: { channel: string }) {
const messages = useQuery(api.messages.list, { channel });
if (messages === undefined) return <p>Loading…</p>;
return (
<ul>
{messages.map((message) => (
<li key={message.id}>{message.body}</li>
))}
</ul>
);
}
function SafeMessageList({ channel }: { channel: string }) {
return (
<ErrorBoundary fallback={<p>Could not load messages.</p>}>
<MessageList channel={channel} />
</ErrorBoundary>
);
}For explicit loading and error UI, use useQueryResult.
Stable args
The hook stores args in a QueryStore keyed by canonical JSON. Object key order does not matter, but the values must be stable across renders to avoid unnecessary resubscription.
// Stable
const args = useMemo(() => ({ channel }), [channel]);
const messages = useQuery(api.messages.list, args);
// Also stable: inline object with same values resubscribes only when channel changes
const messages = useQuery(api.messages.list, { channel });The QueryStore dedupes by canonical wire value, so BigInt, ArrayBuffer, and object ordering do not cause extra watches if the values are semantically equal.
Conditional queries
Pass 'skip' to avoid subscribing until a condition is met:
const userId = useUserId();
const profile = useQuery(api.users.get, userId ? { userId } : 'skip');When unskipped, the store resubscribes. When skipped again, it unsubscribes.
Cleanup
Unsubscribing happens automatically when the component unmounts. useSyncExternalStore handles StrictMode double subscription and cleanup. The returned Unsubscribe function is stored in the QueryStore and is called when the last listener is removed.
SSR limitations
useQuery uses useSyncExternalStore with getServerSnapshot that returns the initial loading state. During renderToString, the store does not subscribe to the client, so useQuery always returns undefined on the server. The same component will load on the client.
// Server: messages === undefined
// Client: messages === undefined initially, then updates
const messages = useQuery(api.messages.list, { channel: 'general' });Mutations with optimistic updates
@pbvex/react does not include a built-in optimistic-update layer. The client does not expose local cache mutation methods. For optimistic UI, maintain local state around the mutation call:
import { useMutation, useQuery } from '@pbvex/react';
import { useState } from 'react';
function LikeButton({ postId }: { postId: string }) {
const sendLike = useMutation(api.posts.like);
const [optimisticLiked, setOptimisticLiked] = useState(false);
async function handleClick() {
setOptimisticLiked(true);
try {
await sendLike({ postId });
} catch {
setOptimisticLiked(false);
}
}
return (
<button onClick={handleClick}>
{optimisticLiked ? 'Liked' : 'Like'}
</button>
);
}The realtime query subscription will reflect the authoritative backend state after the mutation completes.