TanStack Query Integration
TanStack Query integration provides utilities for using oRPC clients with TanStack Query. It includes helper methods for building query and mutation options, as well as query and mutation keys.
WARNING
This guide assumes you are already familiar with TanStack Query. If you need a refresher, review the official TanStack Query documentation before continuing.
Installation
npm install @orpc/tanstack-query@betayarn add @orpc/tanstack-query@betapnpm add @orpc/tanstack-query@betabun add @orpc/tanstack-query@betadeno add npm:@orpc/tanstack-query@betaSetup
Before you begin, set up either a server-side client or a client-side client.
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
const orpc = createTanstackQueryUtils(client)
orpc.planet.find.queryOptions({ input: { id: 123 } })
//
//
//
//
//
//Avoiding Query and Mutation Key Conflicts?
To avoid key conflicts, pass a unique base path when creating each set of utils:
const userORPC = createTanstackQueryUtils(userClient, {
path: ['user']
})
const postORPC = createTanstackQueryUtils(postClient, {
path: ['post']
})Query Options
Use .queryOptions to build query options. It works with useQuery, useSuspenseQuery, and prefetchQuery, and any other API that accepts query options.
const query = useQuery(orpc.planet.find.queryOptions({
input: { id: 123 }, // Specify input if needed
context: { cache: true }, // Provide client context if needed
// additional options...
}))Streamed Query Options
Use .streamedOptions to build streamed query options for Event Iterator. The resulting data is an array of events, and each new event is appended as it arrives.
It works with useQuery, useSuspenseQuery, and prefetchQuery, and any other API that accepts query options.
const query = useQuery(orpc.streamed.streamedOptions({
input: { id: 123 }, // Specify input if needed
context: { cache: true }, // Provide client context if needed
queryFnOptions: { // Configure streamed query behavior
refetchMode: 'reset',
maxChunks: 3,
},
retry: true, // Infinite retry for more reliable streaming
// additional options...
}))INFO
refetchMode determines how data is handled when the query is fetched again:
'reset'(default): Clears existing data and returns the query to a pending state.'append': Adds new streamed chunks to the existing data.'replace': Buffers streamed data and replaces the cache after the stream completes.
Live Query Options
Use .liveOptions to build live query options for Event Iterator. The data always reflects the latest event, replacing the previous value whenever a new one arrives.
It works with useQuery, useSuspenseQuery, and prefetchQuery, and any other API that accepts query options.
const query = useQuery(orpc.live.liveOptions({
input: { id: 123 }, // Specify input if needed
context: { cache: true }, // Provide client context if needed
retry: true, // Infinite retry for more reliable streaming
// additional options...
}))Infinite Query Options
Use .infiniteOptions to build infinite query options. It works with useInfiniteQuery, useSuspenseInfiniteQuery, and prefetchInfiniteQuery, and any other API that accepts infinite query options.
INFO
The input option must be a function that receives the page parameter and returns the query input. Define the pageParam type explicitly if it can be null or undefined.
const query = useInfiniteQuery(orpc.planet.list.infiniteOptions({
input: (pageParam: number | undefined) => ({ limit: 10, offset: pageParam }),
context: { cache: true }, // Provide client context if needed
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.nextPageParam,
// additional options...
}))Mutation Options
Use .mutationOptions to build mutation options. It works with useMutation and any other API that accepts mutation options.
const mutation = useMutation(orpc.planet.create.mutationOptions({
context: { cache: true }, // Provide client context if needed
// additional options...
}))
mutation.mutate({ name: 'Earth' })Query and Mutation Keys
oRPC provides helper methods for generating query and mutation keys:
.key: Generates a partial-match key for actions such as invalidating queries or checking mutation status..queryKey: Generates a full-match key for Query Options..streamedKey: Generates a full-match key for Streamed Query Options..liveKey: Generates a full-match key for Live Query Options..infiniteKey: Generates a full-match key for Infinite Query Options..mutationKey: Generates a full-match key for Mutation Options.
const queryClient = useQueryClient()
// Invalidate all planet queries
queryClient.invalidateQueries({
queryKey: orpc.planet.key(),
})
// Invalidate only regular (non-infinite) planet queries
queryClient.invalidateQueries({
queryKey: orpc.planet.key({ type: 'query' })
})
// Invalidate the planet find query with id 123
queryClient.invalidateQueries({
queryKey: orpc.planet.find.key({ input: { id: 123 } })
})
// Update the planet find query with id 123
queryClient.setQueryData(orpc.planet.find.queryKey({ input: { id: 123 } }), (old) => {
return { ...old, id: 123, name: 'Earth' }
})Calling Clients
The .call method provides direct access to the underlying procedure client when needed.
const planet = await orpc.planet.find.call({ id: 123 })Reactive Options
In reactive libraries like Vue or Solid, TanStack Query supports passing computed values as options. The exact API varies by framework, so refer to the TanStack Query documentation for Vue or Solid.
const query = useQuery(
() => orpc.planet.find.queryOptions({
input: { id: id() },
})
)const query = useQuery(computed(
() => orpc.planet.find.queryOptions({
input: { id: id.value },
})
))Default Options
Use scoped to configure default options for scoped query and mutation utilities. Each value can be either a partial options object, which is spread-merged with lower priority than per-call options, or a function that receives the per-call options and returns the merged result.
const orpc = createTanstackQueryUtils(client, {
scoped: {
planet: {
find: {
queryKey: options => ({
// Override the auto-generated query key for .queryKey and .queryOptions
queryKey: options.queryKey ?? ['planet', 'find', options.input]
}),
queryOptions: {
staleTime: 60 * 1000, // 1 minute
retry: 3,
},
},
list: {
infiniteOptions: options => ({
...options,
staleTime: 30 * 1000, // override takes priority
}),
},
create: {
mutationOptions: {
onSuccess: (output, input, _, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.planet.key() })
},
},
},
},
},
})
// These calls automatically use the default options
const query = useQuery(orpc.planet.find.queryOptions({ input: { id: 123 } }))
const mutation = useMutation(orpc.planet.create.mutationOptions())
// User-provided options take precedence
const customQuery = useQuery(orpc.planet.find.queryOptions({
input: { id: 123 },
staleTime: 0, // overrides the default staleTime
}))INFO
When you configure queryKey, it also affects .queryOptions because it is used internally to generate query keys. The same applies to live, streamed, infinite, and mutation options when you configure their keys.
Interceptors
Interceptors let you wrap queryFn and mutationFn calls. Unlike default options, which can be overridden by per-call options, interceptors always run for every query and mutation.
import { isInferableError, safe } from '@orpc/client'
const orpc = createTanstackQueryUtils(client, {
queryInterceptors: [],
liveInterceptors: [],
streamedInterceptors: [],
infiniteInterceptors: [],
mutationInterceptors: [
async ({ context, path, next }) => {
const [error, data] = await safe(next())
if (error) {
if (isInferableError(error)) {
// handle typesafe errors
}
throw error
}
return data
}
],
scoped: {
planet: {
create: {
mutationInterceptors: [
async ({ next, fnContext }) => {
const result = await next()
fnContext.client.invalidateQueries({ queryKey: orpc.planet.key() })
return result
},
],
},
},
},
})INFO
You can use safe and isInferableError together for typesafe error handling in interceptors.
Plugins
Plugins package reusable defaults and interceptors for queries and mutations.
const orpc = createTanstackQueryUtils(client, {
plugins: []
})Client Context
WARNING
oRPC excludes client context from query keys. Override the query key manually when you need to prevent unintended query deduplication.
const query = useQuery(orpc.planet.find.queryOptions({
context: { cache: true },
// manually include context in the query key
queryKey: [['planet', 'find'], { context: { cache: true } }],
// additional options...
}))When a client is invoked through the TanStack Query integration, an operation context is automatically added to the client context. You can use this context to configure request behavior, such as selecting the HTTP method for RPC Link.
import {
TANSTACK_QUERY_OPERATION_CONTEXT_SYMBOL,
TanstackQueryOperationContext,
} from '@orpc/tanstack-query'
interface ClientContext extends TanstackQueryOperationContext {
}
const GET_OPERATION_TYPE = new Set(['query', 'streamed', 'live', 'infinite'])
const link = new RPCLink<ClientContext>({
method: ({ context }) => {
const operationType = context[TANSTACK_QUERY_OPERATION_CONTEXT_SYMBOL]?.type
if (operationType && GET_OPERATION_TYPE.has(operationType)) {
return 'GET'
}
return 'POST'
},
})Typesafe Error Handling
Use the built-in isInferableError helper to handle typesafe errors in queries and mutations.
import { isInferableError } from '@orpc/client'
const mutation = useMutation(orpc.planet.create.mutationOptions({
onError: (error) => {
if (isInferableError(error)) {
// Handle typesafe errors here
}
}
}))
mutation.mutate({ name: 'Earth' })
if (mutation.error && isInferableError(mutation.error)) {
// Handle the typesafe errors here
}skipToken for Disabling Queries
The skipToken symbol provides a typesafe alternative to setting enabled: false when you want to disable a query by omitting its input.
const query = useQuery(
orpc.planet.list.queryOptions({
input: search ? { search } : skipToken,
})
)
const query = useInfiniteQuery(
orpc.planet.list.infiniteOptions({
input: search
? (offset: number | undefined) => ({ limit: 10, offset, search })
: skipToken,
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.nextPageParam,
})
)Custom Serializers
If needed, you can extend the default TanStack Query serializer to support additional types supported by oRPC. Learn more about RPC Serializers and TanStack Query Server Rendering & Hydration.
import { RPCSerializer } from '@orpc/client'
const serializer = new RPCSerializer({
handlers: {
// put custom serializers here
},
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn(queryKey) {
const serialized = serializer.serialize(queryKey, { useFormDataForBlobFields: false })
return JSON.stringify(serialized)
},
staleTime: 60 * 1000, // > 0 to prevent immediate refetching on mount
},
dehydrate: {
serializeData(data) {
return serializer.serialize(data, { useFormDataForBlobFields: false })
}
},
hydrate: {
deserializeData(data) {
return serializer.deserialize(data)
}
},
}
})
