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:
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
RPCLinkto send fetch requests to/api/rpcwith 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:
| Path | Type | Description |
|---|---|---|
orpc.healthCheck | Query | Public health check. |
orpc.profile.get | Query | Get current user profile. |
orpc.profile.update | Mutation | Update profile settings. |
orpc.documents.list | Query | List user documents. |
orpc.documents.get | Query | Get a single document. |
orpc.documents.delete | Mutation | Delete a document. |
orpc.conversations.list | Query | List conversations. |
orpc.conversations.create | Mutation | Create a conversation. |
orpc.conversations.sendMessage | Mutation | Send a message. |
orpc.conversations.getMessages | Query | Get conversation messages. |
orpc.usage.getSnapshot | Query | Get usage summary. |
orpc.usage.getHistory | Query | Get usage history. |