Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

Pinia Colada Integration

Pinia Colada integration provides utilities for using oRPC clients with Pinia Colada. It includes helper methods for building query and mutation options, as well as query and mutation keys.

Installation

npm install @orpc/pinia-colada@beta
pnpm add @orpc/pinia-colada@beta
yarn add @orpc/pinia-colada@beta
bun add @orpc/pinia-colada@beta

Setup

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

import { createPiniaColadaUtils } from '@orpc/pinia-colada'

const orpc = createPiniaColadaUtils(client)
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 entry key, so entries from different utils never overlap.

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

const postORPC = createPiniaColadaUtils(postClient, {
  prefix: 'post'
})

Query Options Utility

Use .queryOptions to build query options. It works with useQuery 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 Utility

Use .streamedOptions to build streamed query options for an AsyncIteratorObject. The resulting data is an array of chunks, and each new chunk is appended as it arrives. It works with useQuery 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
  fnOptions: { // Configure streamed query behavior
    refetchMode: 'reset',
    maxChunks: 3,
  },
  // additional options...
}))

Live Query Options Utility

Use .liveOptions to build live query options for an AsyncIteratorObject. The data always reflects the latest chunk, replacing the previous value whenever a new one arrives. It works with useQuery 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
  // additional options...
}))

Infinite Query Options Utility

Use .infiniteOptions to build infinite query options. It works with useInfiniteQuery and any other API that accepts infinite query options.

const query = useInfiniteQuery(() => orpc.planet.list.infiniteOptions({
  input: (offset: number) => ({ limit: 10, offset }),
  context: { cache: true }, // Provide client context if needed
  initialPageParam: 0,
  getNextPageParam: lastPage => lastPage.nextOffset,
  // 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/Mutation Key

oRPC provides helper methods for generating query and mutation keys:

const queryCache = useQueryCache()

// Invalidate all planet queries
queryCache.invalidateQueries({
  key: orpc.planet.key(),
})

// Invalidate only regular (non-infinite) planet queries
queryCache.invalidateQueries({
  key: orpc.planet.key({ type: 'query' })
})

// Invalidate the planet find query with id 123
queryCache.invalidateQueries({
  key: orpc.planet.find.key({ input: { id: 123 } })
})

// Update the planet find query with id 123
queryCache.setQueryData(orpc.planet.find.queryKey({ input: { id: 123 } }), (old) => {
  return { ...old, id: 123, name: 'Earth' }
})

Calling Procedure 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

Option utilities accept plain values only. For reactive inputs, pass a callback to useQuery instead — it re-evaluates whenever its dependencies change.

const id = ref(123)

const query = useQuery(() => 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 = createPiniaColadaUtils(client, {
  scoped: {
    planet: {
      find: {
        queryKey: options => ({
          // Override the auto-generated key for .queryKey and .queryOptions
          key: options.key ?? ['planet', 'find', options.input]
        }),
        queryOptions: {
          staleTime: 60 * 1000, // 1 minute
        },
      },
      create: {
        mutationOptions: {
          onSuccess: () => {
            // runs for every planet.create mutation
          },
        },
      },
    },
  },
})

// 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 query and mutation 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 = createPiniaColadaUtils(client, {
  queryInterceptors: [],
  streamedInterceptors: [],
  liveInterceptors: [],
  infiniteInterceptors: [],
  mutationInterceptors: [
    async ({ context, path, next }) => {
      const [error, data] = await safe(next())

      if (error) {
        if (isInferableError(error)) {
          // handle typesafe errors
        }

        throw error
      }

      return data
    }
  ],
})

Plugins

Plugins package reusable defaults and interceptors for queries and mutations.

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

Contract Options Plugin

Use piniaColada 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, piniaColada } from '@orpc/pinia-colada'

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

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

Contracts are defined separately from your app, so anything inside piniaColada cannot import runtime values such as your router utils. Instead, augment UseMutationContextCommon and provide the values through a global onMutate hook, which merges them into the fnContext of every mutation. The example below reads router utils and the query cache from fnContext to optimistically update a query:

import type { RouterContractClient } from '@orpc/contract'
import type { RouterUtils } from '@orpc/pinia-colada'
import type { QueryCache } from '@pinia/colada'

declare module '@pinia/colada' {
  interface UseMutationContextCommon {
    utils: RouterUtils<RouterContractClient<typeof contract>>
    queryCache: QueryCache
  }
}

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

            if (!utils || !queryCache) {
              return next()
            }

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

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

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

app.use(PiniaColada, {
  mutationOptions: {
    onMutate: () => ({
      utils: orpc,
      queryCache: useQueryCache(pinia),
    }),
  },
})

Client Context

When a client is invoked through the Pinia Colada 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 {
  PINIA_COLADA_OPERATION_CONTEXT_SYMBOL,
  PiniaColadaOperationContext,
} from '@orpc/pinia-colada'
import { RPCLink } from '@orpc/client/fetch'

interface ClientContext extends PiniaColadaOperationContext {
}

const GET_OPERATION_TYPE = new Set(['query', 'streamed', 'live', 'infinite'])

const link = new RPCLink<ClientContext>({
  method: ({ context }) => {
    const operationType = context[PINIA_COLADA_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.value && isInferableError(mutation.error.value)) {
  // Handle the typesafe errors here
}

Last updated on August 6, 2026

Was this page helpful?