Raypx

Client-Side Usage

Calling RPC procedures from React components with TanStack Query.

Raypx uses an isomorphic oRPC client that works identically on both server and client. On the server, it calls procedures directly. On the client, it sends HTTP requests to /api/rpc. TanStack Query provides caching, refetching, and optimistic updates.

Isomorphic Client Setup

The client is configured in src/utils/orpc.ts:

src/utils/orpc.ts
import { createORPCClient } from "@orpc/client"
import { RPCLink } from "@orpc/client/fetch"
import { createRouterClient } from "@orpc/server"
import { createTanstackQueryUtils } from "@orpc/tanstack-query"
import { createContext } from "@raypx/rpc/context"
import { appRouter } from "@raypx/rpc/routers/index"
import { QueryCache, QueryClient } from "@tanstack/react-query"
import { createIsomorphicFn } from "@tanstack/react-start"

export const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      toast.error(`Error: ${error.message}`, {
        action: { label: "retry", onClick: query.invalidate },
      })
    },
  }),
})

const getORPCClient = createIsomorphicFn()
  .server(() =>
    createRouterClient(appRouter, {
      context: async () => createContext({ req: getRequest() }),
    }),
  )
  .client((): RouterClient<typeof appRouter> => {
    const link = new RPCLink({
      url: `${window.location.origin}/api/rpc`,
      fetch(url, options) {
        return fetch(url, { ...options, credentials: "include" })
      },
    })
    return createORPCClient(link)
  })

export const client: RouterClient<typeof appRouter> = getORPCClient()

export const orpc = createTanstackQueryUtils(client)

Key points:

  • createIsomorphicFn() -- TanStack Start utility that provides different implementations for server and client.
  • Server side -- Uses createRouterClient() to call procedures directly (no HTTP overhead).
  • Client side -- Uses RPCLink to send fetch requests to /api/rpc with cookie credentials.
  • orpc -- TanStack Query utilities with full type inference.

TanStack Query Integration

The createTanstackQueryUtils function creates type-safe hooks that integrate with TanStack Query:

Reading Data with useQuery

import { orpc } from "@/utils/orpc"

function ProfilePage() {
  const { data: profile, isPending, error } = orpc.profile.get.useQuery({})

  if (isPending) return <Skeleton />
  if (error) return <p>Error loading profile</p>

  return (
    <div>
      <h1>{profile?.defaultModel ?? "No model set"}</h1>
      <p>Locale: {profile?.preferredLocale}</p>
    </div>
  )
}

Writing Data with useMutation

import { orpc } from "@/utils/orpc"

function UpdateProfile() {
  const utils = orpc.useUtils()

  const mutation = orpc.profile.update.useMutation({
    onSuccess: () => {
      toast.success("Profile updated")
      utils.profile.get.invalidate() // Refetch the profile
    },
    onError: (error) => {
      toast.error(error.message)
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        const formData = new FormData(e.currentTarget)
        mutation.mutate({
          defaultModel: formData.get("model") as string,
          preferredLocale: formData.get("locale") as string,
        })
      }}
    >
      <input name="model" />
      <input name="locale" />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? "Saving..." : "Save"}
      </button>
    </form>
  )
}

Example: Fetching Documents

function DocumentsList() {
  const { data: documents, isPending } = orpc.documents.list.useQuery({})

  if (isPending) return <Skeleton />

  return (
    <ul>
      {documents?.map((doc) => (
        <li key={doc.id}>
          {doc.title} -- {doc.status}
        </li>
      ))}
    </ul>
  )
}

Example: Creating a Conversation

function NewConversation() {
  const utils = orpc.useUtils()

  const create = orpc.conversations.create.useMutation({
    onSuccess: (data) => {
      toast.success("Conversation created")
      utils.conversations.list.invalidate()
      navigate({ to: `/conversations/${data.id}` })
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        const title = new FormData(e.currentTarget).get("title") as string
        create.mutate({ title })
      }}
    >
      <input name="title" required />
      <button type="submit">Create</button>
    </form>
  )
}

Error Handling

The QueryClient is configured with a global onError handler that shows a toast with a retry button:

export const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      toast.error(`Error: ${error.message}`, {
        action: { label: "retry", onClick: query.invalidate },
      })
    },
  }),
})

For per-mutation error handling, use the onError callback in useMutation.

Available Procedures

All procedures are accessible via the orpc object, mirroring the server-side appRouter structure:

PathTypeDescription
orpc.healthCheckQueryPublic health check.
orpc.profile.getQueryGet current user profile.
orpc.profile.updateMutationUpdate profile settings.
orpc.documents.listQueryList user documents.
orpc.documents.getQueryGet a single document.
orpc.documents.deleteMutationDelete a document.
orpc.conversations.listQueryList conversations.
orpc.conversations.createMutationCreate a conversation.
orpc.conversations.sendMessageMutationSend a message.
orpc.conversations.getMessagesQueryGet conversation messages.
orpc.usage.getSnapshotQueryGet usage summary.
orpc.usage.getHistoryQueryGet usage history.

On this page