Next.js

Full App Router adapter with RSC prefetch and hydration for @pyrpc/next.

@pyrpc/next extends @pyrpc/react with React Server Component helpers: api.prefetch, api.dehydrate(), and api.HydrationBoundary. Server components warm the TanStack cache; client components call useQuery and get instant data with no loading flicker.

Installation

npm install @pyrpc/next @tanstack/react-query

Project structure

my-app/
  app/
    layout.tsx        ← RootLayout with <Providers>
    page.tsx          ← Server component: prefetch
    counter.tsx       ← Client component: useQuery/useMutation
    providers.tsx     ← "use client" QueryClient + api.Provider
  lib/
    pyrpc.ts          ← createNextClient, import this everywhere

1. Create the client

lib/pyrpc.ts
import { createNextClient, httpBatchLink } from "@pyrpc/next"
import type { Types } from "@pyrpc/types"

export const api = createNextClient<Types>({
  links: [
    httpBatchLink({
      url: process.env.PYRPC_URL ?? "http://localhost:8000",
    }),
  ],
})

One file, one import everywhere, same variable on the server and in client components.

2. Set up Providers

app/providers.tsx
"use client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { api } from "@/lib/pyrpc"
import { useState } from "react"

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient())
  return (
    <QueryClientProvider client={queryClient}>
      <api.Provider>{children}</api.Provider>
    </QueryClientProvider>
  )
}
app/layout.tsx
import { Providers } from "./providers"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

3. Server component: prefetch

Prefetch in a Server Component so the client gets data on first render with no loading state:

app/page.tsx
import { api } from "@/lib/pyrpc"
import { Counter } from "./counter"

export default async function Page() {
  // warm the cache server-side
  await api.prefetch.read_root()
  await api.prefetch.read_item({ item_id: 42, q: "test" })

  return (
    <api.HydrationBoundary state={api.dehydrate()}>
      <Counter />
    </api.HydrationBoundary>
  )
}

4. Client component: hooks

app/counter.tsx
"use client"
import { api } from "@/lib/pyrpc"
import { useState } from "react"

export function Counter() {
  const [name, setName] = useState("")

  // data is already in cache, no loading flicker
  const { data: greeting } = api.read_root.useQuery()
  const { data: item } = api.read_item.useQuery({ item_id: 42, q: "test" })
  const createItem = api.create_item.useMutation()

  return (
    <div>
      <pre>{JSON.stringify(greeting)}</pre>
      <pre>{JSON.stringify(item)}</pre>

      <input value={name} onChange={e => setName(e.target.value)} />
      <button
        onClick={() => createItem.mutate({ name, description: `Item: ${name}` })}
        disabled={createItem.isPending}
      >
        {createItem.isPending ? "Creating…" : "Create"}
      </button>
      {createItem.isSuccess && <pre>{JSON.stringify(createItem.data)}</pre>}
    </div>
  )
}

What's on api

PropertyWhereDescription
api.<proc>.useQuery(…)ClientTanStack useQuery hook
api.<proc>.useMutation(…)ClientTanStack useMutation hook
api.ProviderClientProvides the TanStack cache
api.useUtils()ClientCache utilities (invalidate, setData…)
api.prefetch.<proc>(…)ServerWarms the cache before rendering
api.dehydrate()ServerSerializes the cache for handoff
api.HydrationBoundaryServer → ClientPasses dehydrated cache to the browser
api.createCaller()ServerDirect Promise calls (queries + mutations)

Skipping prefetch

Prefetch is optional. If you skip it, the client component fetches on mount exactly like plain @pyrpc/react:

app/page.tsx
// no prefetch
export default function Page() {
  return <Counter />
}

Server-side mutations with createCaller

Mutations can't be prefetched (they have side effects), but you can call them from Server Actions or Route Handlers:

app/actions.ts
"use server"
import { api } from "@/lib/pyrpc"

export async function serverCreate(name: string) {
  const caller = api.createCaller()
  return caller.create_item({ name })
}

Full working examples