Skip to content
You are reading the v2 docs, currently in beta.V1 docs
oRPC
Esc
↑↓navigate↵open⌘Jpreview
On this page

TanStack Query Integration

Integrate oRPC with TanStack Query using utilities for query and mutation options, keys, infinite and streamed queries, and typesafe errors.

Installation

npm install @orpc/tanstack-query@beta
pnpm add @orpc/tanstack-query@beta
yarn add @orpc/tanstack-query@beta
bun add @orpc/tanstack-query@beta
nub add @orpc/tanstack-query@beta
aube add @orpc/tanstack-query@beta

Setup

Before you begin, set up either a server-side client or a client-side client.

import { function createTanstackQueryUtils<T extends AnyNestedClient>(client: T, options?: NoInfer<RouterUtilsOptions<T>>): RouterUtils<T>
Creates TanStack Query utils from a client, exposing query/mutation option builders for every procedure in the router.
@remarks**Note**: Both client-side and server-side clients are supported.@see{@link https://orpc.dev/docs/integrations/tanstack-query TanStack Query Integration}
createTanstackQueryUtils
} from '@orpc/tanstack-query'
const
const orpc: {
    planet: {
        list: Public<ProcedureUtils<object, {
            limit?: number | undefined;
            cursor?: number | undefined;
        }, {
            id: number;
            name: string;
            description?: string | undefined;
        }[], Error>>;
        find: Public<ProcedureUtils<object, {
            id: number;
        }, {
            id: number;
            name: string;
            description?: string | undefined;
        }, Error>>;
        create: Public<ProcedureUtils<object, {
            name: string;
            description?: string | undefined;
        }, {
            id: number;
            name: string;
            description?: string | undefined;
        }, Error>>;
    } & Public<...>;
} & Public<...>
orpc
=
createTanstackQueryUtils<{
    planet: {
        list: ProcedureClient<object, ZodObject<{
            limit: ZodOptional<ZodNumber>;
            cursor: ZodDefault<ZodNumber>;
        }, $strip>, ZodArray<ZodObject<{
            id: ZodNumber;
            name: ZodString;
            description: ZodOptional<ZodString>;
        }, $strip>>, Record<never, never>>;
        find: ProcedureClient<object, ZodObject<{
            id: ZodNumber;
        }, $strip>, ZodObject<{
            id: ZodNumber;
            name: ZodString;
            description: ZodOptional<ZodString>;
        }, $strip>, Record<...>>;
        create: ProcedureClient<...>;
    };
}>(client: {
    planet: {
        list: ProcedureClient<object, ZodObject<{
            limit: ZodOptional<ZodNumber>;
            cursor: ZodDefault<ZodNumber>;
        }, $strip>, ZodArray<ZodObject<{
            id: ZodNumber;
            name: ZodString;
            description: ZodOptional<ZodString>;
        }, $strip>>, Record<never, never>>;
        find: ProcedureClient<object, ZodObject<{
            id: ZodNumber;
        }, $strip>, ZodObject<{
            id: ZodNumber;
            name: ZodString;
            description: ZodOptional<ZodString>;
        }, $strip>, Record<...>>;
        create: ProcedureClient<...>;
    };
}, options?: NoInfer<RouterUtilsOptions<...>>): {
    ...;
} & Public<...>
Creates TanStack Query utils from a client, exposing query/mutation option builders for every procedure in the router.
@remarks**Note**: Both client-side and server-side clients are supported.@see{@link https://orpc.dev/docs/integrations/tanstack-query TanStack Query Integration}
createTanstackQueryUtils
(
const client: {
    planet: {
        list: ProcedureClient<object, ZodObject<{
            limit: ZodOptional<ZodNumber>;
            cursor: ZodDefault<ZodNumber>;
        }, $strip>, ZodArray<ZodObject<{
            id: ZodNumber;
            name: ZodString;
            description: ZodOptional<ZodString>;
        }, $strip>>, Record<never, never>>;
        find: ProcedureClient<object, ZodObject<{
            id: ZodNumber;
        }, $strip>, ZodObject<{
            id: ZodNumber;
            name: ZodString;
            description: ZodOptional<ZodString>;
        }, $strip>, Record<...>>;
        create: ProcedureClient<...>;
    };
}
client
)
const orpc: {
    planet: {
        list: Public<ProcedureUtils<object, {
            limit?: number | undefined;
            cursor?: number | undefined;
        }, {
            id: number;
            name: string;
            description?: string | undefined;
        }[], Error>>;
        find: Public<ProcedureUtils<object, {
            id: number;
        }, {
            id: number;
            name: string;
            description?: string | undefined;
        }, Error>>;
        create: Public<ProcedureUtils<object, {
            name: string;
            description?: string | undefined;
        }, {
            id: number;
            name: string;
            description?: string | undefined;
        }, Error>>;
    } & Public<...>;
} & Public<...>
orpc
.
planet: {
    list: Public<ProcedureUtils<object, {
        limit?: number | undefined;
        cursor?: number | undefined;
    }, {
        id: number;
        name: string;
        description?: string | undefined;
    }[], Error>>;
    find: Public<ProcedureUtils<object, {
        id: number;
    }, {
        id: number;
        name: string;
        description?: string | undefined;
    }, Error>>;
    create: Public<ProcedureUtils<object, {
        name: string;
        description?: string | undefined;
    }, {
        id: number;
        name: string;
        description?: string | undefined;
    }, Error>>;
} & Public<...>
planet
.
find: Public<ProcedureUtils<object, {
    id: number;
}, {
    id: number;
    name: string;
    description?: string | undefined;
}, Error>>
find
.
  • call
  • infiniteKey
  • infiniteOptions
  • key
  • liveKey
  • liveOptions
  • mutationKey
  • mutationOptions
  • queryKey
  • queryOptions
  • streamedKey
  • streamedOptions
queryOptions<{
    id: number;
    name: string;
    description?: string | undefined;
}, undefined>(options: QueryOptionsIn<object, {
    id: number;
}, {
    id: number;
    name: string;
    description?: string | undefined;
}, Error, {
    id: number;
    name: string;
    description?: string | undefined;
}, undefined>): NoInfer<QueryOptionsOut<{
    id: number;
    name: string;
    description?: string | undefined;
}, Error, {
    id: number;
    name: string;
    description?: string | undefined;
}, undefined>>
Generate options used for useQuery/useSuspenseQuery/prefetchQuery/...
queryOptions
({
input: {
    id: number;
} | unique symbol
input
: { id: numberid: 123 } })
// // // // // //
Avoiding Query and Mutation Key Conflicts?

To avoid key conflicts when creating multiple sets of utils, pass a unique prefix. It becomes the first element of every query/mutation key, so keys from different utils never overlap.

const userORPC = createTanstackQueryUtils(userClient, {
  prefix: 'user'
})

const postORPC = createTanstackQueryUtils(postClient, {
  prefix: '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 an AsyncIteratorObject. 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...
}))

Live Query Options

Use .liveOptions to build live query options for an AsyncIteratorObject. 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.

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:

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' }
})

Key Serializer

oRPC serializes inputs with the RPC JSON Serializer when building keys, so inputs like Date, BigInt, Set, and Map match by value, and a Date and its ISO string produce different keys. You can pass a custom RPC JSON Serializer, or an OpenAPI JSON Serializer if you don’t need type-aware keys:

const orpc = createTanstackQueryUtils(client, { serializer })

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
}))

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 { isDefinedError, 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 (isDefinedError(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
          },
        ],
      },
    },
  },
})

Plugins

Plugins package reusable defaults and interceptors for queries and mutations.

const orpc = createTanstackQueryUtils(client, {
  plugins: []
})

Contract Options Plugin

Use tanstackQuery to define base options and interceptors directly on a procedure contract, then pass the contract to ContractOptionsUtilsPlugin to apply them automatically. Meta options act as the base layer: default options and interceptors defined on the utils merge on top of them. Passing undefined explicitly for a key resets the value from lower layers instead of merging.

import { ContractOptionsUtilsPlugin, tanstackQuery } from '@orpc/tanstack-query'

export const contract = {
  planet: {
    find: oc
      .input(z.object({ id: z.number() }))
      .meta(tanstackQuery({
        queryOptions: {
          staleTime: 60 * 1000,
        },
        queryInterceptors: [
          async ({ input, next }) => {
            // input, output, and errors are typed based on the contract
            return await next()
          },
        ],
      })),
  },
}

const orpc = createTanstackQueryUtils(client, {
  plugins: [new ContractOptionsUtilsPlugin(contract)],
})
Passing runtime values into contract meta?

Contracts are defined separately from your app, so anything inside tanstackQuery cannot import runtime values such as your router utils. Instead, register a global meta type and pass the values through the meta option, per hook or globally via query client default options. The example below reads router utils from fnContext.meta to optimistically update a query:

import type { RouterContractClient } from '@orpc/contract'
import type { RouterUtils } from '@orpc/tanstack-query'

declare module '@tanstack/react-query' {
  interface Register {
    mutationMeta: {
      utils?: RouterUtils<RouterContractClient<typeof contract>>
    }
  }
}

export const contract = {
  planet: {
    find: oc.input(z.object({ id: z.number() })),
    update: oc
      .input(z.object({ id: z.number(), name: z.string() }))
      .meta(tanstackQuery({
        mutationInterceptors: [
          async ({ input, next, fnContext }) => {
            const utils = fnContext.meta?.utils

            if (!utils) {
              return next()
            }

            const queryKey = utils.planet.find.queryKey({ input: { id: input.id } })
            const previous = fnContext.client.getQueryData(queryKey)

            // optimistically update before the request
            fnContext.client.setQueryData(queryKey, input)

            try {
              return await next()
            }
            catch (error) {
              // roll back on error
              fnContext.client.setQueryData(queryKey, previous)
              throw error
            }
            finally {
              fnContext.client.invalidateQueries({ queryKey })
            }
          },
        ],
      })),
  },
}

const queryClient = new QueryClient({
  defaultOptions: {
    mutations: {
      meta: { utils: orpc },
    },
  },
})

Client Context

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 {
  const TANSTACK_QUERY_OPERATION_CONTEXT_SYMBOL: typeof TANSTACK_QUERY_OPERATION_CONTEXT_SYMBOL
The symbol under which TanStack Query utils attach operation details (key and operation type) to the client context.
@see{@link https://orpc.dev/docs/integrations/tanstack-query#client-context TanStack Query Integration - Client Context}
TANSTACK_QUERY_OPERATION_CONTEXT_SYMBOL
,
TanstackQueryOperationContext, } from '@orpc/tanstack-query' interface ClientContext extends TanstackQueryOperationContext { } const const GET_OPERATION_TYPE: Set<string>GET_OPERATION_TYPE = new
var Set: SetConstructor
new <string>(iterable?: Iterable<string> | null | undefined) => Set<string> (+1 overload)
Set
(['query', 'streamed', 'live', 'infinite'])
const const link: RPCLink<ClientContext>link = new new RPCLink<ClientContext>(options: RPCLinkOptions<ClientContext>): RPCLink<ClientContext>
Client link that communicates with an RPC Handler over the Fetch API (HTTP).
@see{@link https://orpc.dev/docs/adapters/fetch-api Fetch API Adapter}
RPCLink
<ClientContext>({
RPCLinkCodecOptions<ClientContext>.method?: Value<Promisable<"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "QUERY">, [options: ClientOptions<ClientContext>, path: string[], input: unknown]> | undefined
The method used to make the request.
@default'POST'
method
: ({ context: ClientContextcontext }) => {
const const operationType: OperationType | undefinedoperationType = context: ClientContextcontext[const TANSTACK_QUERY_OPERATION_CONTEXT_SYMBOL: typeof TANSTACK_QUERY_OPERATION_CONTEXT_SYMBOL
The symbol under which TanStack Query utils attach operation details (key and operation type) to the client context.
@see{@link https://orpc.dev/docs/integrations/tanstack-query#client-context TanStack Query Integration - Client Context}
TANSTACK_QUERY_OPERATION_CONTEXT_SYMBOL
]?.type: OperationType | undefinedtype
if (const operationType: OperationType | undefinedoperationType && const GET_OPERATION_TYPE: Set<string>GET_OPERATION_TYPE.Set<string>.has(value: string): boolean
@returnsa boolean indicating whether an element with the specified value exists in the Set or not.
has
(const operationType: OperationTypeoperationType)) {
return 'GET' } return 'POST' }, })

Typesafe Error Handling

Use the built-in isDefinedError helper to handle typesafe errors in queries and mutations.

import { isDefinedError } from '@orpc/client'

const mutation = useMutation(orpc.planet.create.mutationOptions({
  onError: (error) => {
    if (isDefinedError(error)) {
      // Handle typesafe errors here
    }
  }
}))

mutation.mutate({ name: 'Earth' })

if (mutation.error && isDefinedError(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,
  })
)

Structural Sharing

.queryOptions, .liveOptions, and .infiniteOptions use oRPC’s structural sharing by default, and this per-query setting takes precedence over defaultOptions.queries.structuralSharing on the QueryClient. Unlike TanStack Query’s default structural sharing, it also keeps unchanged Date, URL, Map, and Set values, so refetches and live events that return the same data do not cause re-renders.

import { createStructuralSharing } from '@orpc/tanstack-query'

// Opt out per call
orpc.planet.list.queryOptions({ input: { limit: 10 }, structuralSharing: false })

// Use it for queries not built with oRPC utils
const queryClient = new QueryClient({
  defaultOptions: { queries: { structuralSharing: createStructuralSharing() } },
})

With an RPC key serializer, equal instances of custom types are kept too, compared by what their handler serializes them to. For queries not built with oRPC utils, pass it to createStructuralSharing(serializer).

Server-Side Rendering (SSR)

When server-side rendering with TanStack Query hydration, two things need special care: oRPC types that JSON cannot represent, and streamed or live queries that never complete.

Custom Serializers

TanStack Query dehydrates query data as plain JSON, which cannot represent oRPC types such as Date, BigInt, Set, or Map. Pass your RPC Serializer to createHydrationOptions to keep them intact.

import { createHydrationOptions } from '@orpc/tanstack-query'
import { QueryClient } from '@tanstack/react-query' // or any framework adapter, e.g. @tanstack/vue-query

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // > 0 to prevent immediate refetching on mount
    },
    ...createHydrationOptions(serializer), // dehydrate.serializeData and hydrate.deserializeData
  },
})

Streamed and Live Queries

StreamQuerySSRUtilsPlugin lets streamed and live queries render on the server without blocking forever: streamed queries are cancelled once they connect and live queries on their first event, keeping the data received so far. In the browser, they refetch on mount to reopen the stream after hydration.

import { StreamQuerySSRUtilsPlugin } from '@orpc/tanstack-query'

export const orpc = createTanstackQueryUtils(client, {
  plugins: [new StreamQuerySSRUtilsPlugin()],
})

An environment without window counts as the server; pass isServer to override.

Last updated on

Was this page helpful?