-
Hello everyone. I'm having a bit of a problem making protected procedures (the ones that require auth) to work correctly with Tanstack Query for hydration. I created these two basic reusable procedures below: export const publicProcedure = os.use(async ({ next }) => {
const session = await auth();
const result = await next({
context: {
session,
db,
},
});
return result;
});
export const protectedProcedure = publicProcedure.use(
async ({ context, next }) => {
const session = context.session;
if (!session) {
throw new ORPCError("UNAUTHORIZED", {
message: "You must be logged in to access this resource.",
});
}
const result = await next({
context: {
...context,
session: session,
},
});
return result;
},
); This is my router:import { protectedProcedure, publicProcedure } from "@/orpc/procedures";
import { z } from "zod";
const hello = publicProcedure
.input(z.object({ text: z.string() }))
.handler(({ input }) => {
return {
greeting: `Hello ${input.text}`,
};
})
.callable();
const create = protectedProcedure
.input(z.object({ name: z.string().min(1) }))
.handler(async ({ context, input }) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return await context.db.post.create({
data: {
name: input.name,
createdBy: { connect: { id: context.session.user.id } },
},
});
})
.callable();
const getLatest = protectedProcedure
.handler(async ({ context }) => {
const post = await context.db.post.findFirst({
orderBy: { createdAt: "desc" },
where: { createdBy: { id: context.session.user.id } },
});
return post ?? null;
})
.callable();
const getSecretMessage = protectedProcedure
.handler(async () => {
return "you can now see this secret message!";
})
.callable();
export const router = {
hello,
create,
getLatest,
getSecretMessage,
}; Providers.tsx:"use client";
import { orpc } from "@/server/orcp/client";
import { ORPCContext } from "@/server/orcp/context";
import { getQueryClient } from "@/server/orcp/query-client";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
export default function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>
<ORPCContext.Provider value={orpc}>{children}</ORPCContext.Provider>
</ReactQueryStreamedHydration>
</QueryClientProvider>
);
} Calling from the server:export default async function Home() {
// THIS WORKS, PUBLIC PROCEDURE
const hello = await orpc.post.hello.call({ text: "from ORPC" });
// THIS FAILS, NOT AUTHORIZED
const message = await orpc.post.getSecretMessage.call();
// THIS WORKS, AUTHORIZED
const message2 = await router.post.getSecretMessage();
return (...)
} Calling from the client:export function LatestPost() {
const [name, setName] = useState("");
const orpc = useORPC();
// THIS WORKS, AUTHORIZED, NOT HYDRATED
const latestPost = useQuery(orpc.post.getLatest.queryOptions());
// THIS FAILS ON FIRST TRY, NOT AUTHORIZED, IT RETRIES CONNECTION AGAIN AUTOMATICALLY AND IT WORKS
const latestPost = useSuspenseQuery(orpc.post.getLatest.queryOptions());
return (...)
} This is what the console output looks like when using only useSuspenseQuery after a page refresh:POST /rpc/post/hello 200 in 39ms
POST /rpc/post/getLatest 401 in 35ms <-------- FIRST TRY FAILS
⨯ [Error: You must be logged in to access this resource.] {
defined: false,
code: 'UNAUTHORIZED',
status: 401,
data: undefined,
digest: '1326613100'
}
GET / 500 in 448ms
POST /rpc/post/getLatest 200 in 41ms <-------- SECOND TRY WORKS The only way I found to make this work is by doing this:export default async function Home() {
const queryClient = getQueryClient();
await queryClient.prefetchQuery(
orpc.post.getLatest.queryOptions({
queryFn: router.post.getLatest,
}),
);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<LatestPost />
</HydrationBoundary>
)
} |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 2 replies
-
I tried injecting headers when creating the RPCLink, but they either come up empty or it fails with a message that "headers can't be called on the client side". I was wondering if there is a better way to make this work without having to use HydrationBoundary on every component I try to prefetch. Some docs for those interested: This is my repository if anyone wants to give it a try: |
Beta Was this translation helpful? Give feedback.
-
You can pass headers to the RPCLink. However currently you are just creating one shared RPCLink and client for all requests (https://github.com/MaKTaiL/orpc-test-next/blob/main/src/server/orcp/client.ts#L13-L17). Instead you should do that in your root component: https://github.com/MaKTaiL/orpc-test-next/blob/main/src/app/providers.tsx#L40-L50. So in there add: const [link] = useState(() => new RPCLink({ url: getBaseUrl() + "/rpc", headers: () => (typeof window === 'undefined' ? Object.fromEntries(headers().entries()) : ({}) }));
const [client] = useState<RouterClient<typeof router>>(() => createORPCClient(link));
const [orpc] = useState(() => createORPCReactQueryUtils(client)); Note that currently you are also creating a new query client whenever the Providers component renders. I don't use nextjs so I don't know if providers.tsx is a special file that is guaranteed to render just once, if not I recommend replacing that line with: const [queryClient] = useState(() => makeQueryClient()); Let me know if it works, I didn't test anything |
Beta Was this translation helpful? Give feedback.
-
@timvandam Your idea for creating the RPCLink inside the provider was PERFECT!! IT WORKED! Thank you! I was struggling to find a way to send the headers to RPCLink and it was simpler than I imagined. This allows me to take advantage of the new @unnoq if you want to add future instructions in the website for Next.js + Tanstack Query setup, here is what I did (I also updated my repository if you want to test it out): layout.tsximport "@/styles/globals.css";
import { type Metadata } from "next";
import { Geist } from "next/font/google";
import Providers from "./providers";
import { headers } from "next/headers";
export const metadata: Metadata = {
title: "Create T3 App",
description: "Generated by create-t3-app",
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
const geist = Geist({
subsets: ["latin"],
variable: "--font-geist-sans",
});
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export default async function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
const heads = await headers();
const cookie = heads.get("Cookie") ?? undefined;
const baseUrl = getBaseUrl();
return (
<html lang="en" className={`${geist.variable}`}>
<body>
<Providers baseUrl={baseUrl} cookie={cookie}>
{children}
</Providers>
</body>
</html>
);
} providers.tsx"use client";
import type { router } from "@/server/orcp";
import { ORPCContext } from "@/server/orcp/context";
import { getQueryClient } from "@/server/orcp/query-client";
import { createORPCClient } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";
import { createORPCReactQueryUtils } from "@orpc/react-query";
import type { RouterClient } from "@orpc/server";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
export default function Providers({
children,
baseUrl,
cookie,
}: {
children: React.ReactNode;
baseUrl: string;
cookie?: string;
}) {
const queryClient = getQueryClient();
const link = new RPCLink({
url: baseUrl + "/rpc",
headers: {
Cookie: cookie,
},
});
const client: RouterClient<typeof router> = createORPCClient(link);
const orpc = createORPCReactQueryUtils(client);
return (
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>
<ORPCContext.Provider value={orpc}>{children}</ORPCContext.Provider>
</ReactQueryStreamedHydration>
</QueryClientProvider>
);
} post.tsx"use client";
import { useORPC } from "@/server/orcp/context";
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { useState } from "react";
export function LatestPost() {
const [name, setName] = useState("");
const orpc = useORPC();
const latestPost = useSuspenseQuery(orpc.post.getLatest.queryOptions());
const createPost = useMutation(
orpc.post.create.mutationOptions({
onSuccess() {
void latestPost.refetch();
setName("");
},
}),
);
return (
<div className="w-full max-w-xs">
{latestPost ? (
<p className="truncate">
Your most recent post: {latestPost.data?.name ?? "No name available"}
</p>
) : (
<p>You have no posts yet.</p>
)}
<form
onSubmit={(e) => {
e.preventDefault();
createPost.mutate({ name });
}}
className="flex flex-col gap-2"
>
<input
type="text"
placeholder="Title"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-full bg-white/10 px-4 py-2 text-white"
/>
<button
type="submit"
className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
disabled={createPost.isPending}
>
{createPost.isPending ? "Submitting..." : "Submit"}
</button>
</form>
</div>
);
} |
Beta Was this translation helpful? Give feedback.
-
We just write about Optimize SSR, check it here: https://orpc.unnoq.com/docs/best-practices/optimize-ssr |
Beta Was this translation helpful? Give feedback.
-
Very interesting approach with globalThis, I'll try on my side, thanks for taking the time to write this. |
Beta Was this translation helpful? Give feedback.
You can pass headers to the RPCLink. However currently you are just creating one shared RPCLink and client for all requests (https://github.com/MaKTaiL/orpc-test-next/blob/main/src/server/orcp/client.ts#L13-L17).
Instead you should do that in your root component: https://github.com/MaKTaiL/orpc-test-next/blob/main/src/app/providers.tsx#L40-L50.
So in there add:
Note that…