I am learning T3 stack with App router. But I ran into a problem with using tRPC. 90% tutorial in the Internet shows how to combine tRPC with next js using hooks, for example:
const {data: posts, isLoading} = api.posts.all.useQuery({});
But it's a hook! It can be rendered only in a client component. So it kills SSR, SSG, whatever.
IDK why everybody on youtube shows this way to get data. I have watched a video like this on the official Theo T3
YT channel.
So, how to correctly get data in server components using tRPC with next.js?
I found 2 ways, but I haven't fully figured them out.
- Way 1 - using
createTRPCProxyClient
that T3 CLI provides. I create tRPCserverApi
, then in a server component I load data and pass it as props to CLIENT component that use tRPC hook useQuery with initialData. It works, okay, but it's so complex, isn't it? Moreover, it requiresnoStore()
call before everyserverAPI
- it's so confusing! WitoutnoStore()
dev server works absolutely fine, but when I runnpm run build
I get an error. P.S. sorry for a lot of code
// trpc/server.ts
import "server-only";
import {
createTRPCProxyClient,
loggerLink,
TRPCClientError,
} from "@trpc/client";
import { callProcedure } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { type TRPCErrorResponse } from "@trpc/server/rpc";
import { headers } from "next/headers";
import { cache } from "react";
import { appRouter, type AppRouter } from "@/server/api/root";
import { createTRPCContext } from "@/server/api/trpc";
import { transformer } from "./shared";
/**
* This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
* handling a tRPC call from a React Server Component.
*/
const createContext = cache(() => {
const heads = new Headers(headers());
heads.set("x-trpc-source", "rsc");
return createTRPCContext({
headers: heads,
});
});
export const serverApi = createTRPCProxyClient<AppRouter>({
transformer,
links: [
/**
* Custom RSC link that lets us invoke procedures without using http requests. Since Server
* Components always run on the server, we can just call the procedure as a function.
*/
() =>
({ op }) =>
observable((observer) => {
createContext()
.then((ctx) => {
return callProcedure({
procedures: appRouter._def.procedures,
path: op.path,
rawInput: op.input,
ctx,
type: op.type,
});
})
.then((data) => {
observer.next({ result: { data } });
observer.complete();
})
.catch((cause: TRPCErrorResponse) => {
observer.error(TRPCClientError.from(cause));
});
}),
],
});
// app/page.tsx
// ...
import { unstable_noStore as noStore } from "next/cache";
import { serverApi } from "@/trpc/server";
const ResentTweetsPage: NextPage = async ({}) => {
noStore();
// ^^^^^^^^ How to remove it?
const tweets = await serverApi.tweet.infiniteFeed.query(
{},
);
return (
<TabsWrapper
current="Recent"
>
<RecentTweets
initialTweets={tweets}
/>
</TabsWrapper>
);
};
export default ResentTweetsPage
// components/RecentTweets
'use client';
import { api } from "@/trpc/react";
import { serverApi } from "@/trpc/server";
export const RecentTweets: FC<{
initialTweets: Awaited<ReturnType<(typeof serverApi.tweet.infiniteFeed.query)>>
}> = ({
initialTweets
}) => {
const tweets = api.tweet.infiniteFeed.useInfiniteQuery(
{},
{
getNextPageParam: lastPage => lastPage.nextCursor,
// Thanks for it the page render this data at first loading
initialData: {
pageParams: [],
pages: [
initialTweets
]
},
refetchOnMount: false,
refetchOnReconnect: false,
refetchInterval: 1000 * 60
}
);
return (
<InfiniteTweetList
tweets={
tweets.data?.pages.flatMap(page => page.tweets)
}
isError={tweets.isError}
isLoading={tweets.isLoading}
hasMore={Boolean(tweets.hasNextPage)}
fetchNewTweets={tweets.fetchNextPage}
/>
);
};
- Way 2 - using
createCaller
. I still need to passinitialTweets
to client component, and there's no.query()
inawait serverClient.tweet.infiniteFeed({})
. I would prefer this method, but when I runnpm run build
, I get this error:Page couldn't be rendered statically because it used 'headers'
and I haven't found how to fix that.
// trpc/serverClient.ts
import { appRouter } from "@/server/api/root";
import { httpBatchLink } from "@trpc/client";
import { createTRPCContext } from "@/server/api/trpc";
export const serverClient = appRouter.createCaller(
await createTRPCContext({} as any)
)
// app/page.tsx
import {NextPage} from 'next';
import React from "react";
import { RecentTweets } from "@/components/RecentTweets";
import { serverClient } from "@/trpc/serverClient";
import { TabsWrapper } from "@/app/feed/components/TabsWrapper";
const ResentTweetsPage: NextPage = async ({}) => {
const tweets = await serverClient.tweet.infiniteFeed(
{}
);
return (
<TabsWrapper
current="Recent"
>
<RecentTweets
initialTweets={tweets}
/>
</TabsWrapper>
);
};
export default ResentTweetsPage
Thanks you for your help