Next.js how to use tRPC with a server components correctly
Asked Answered
H

0

7

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 tRPC serverApi, 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 requires noStore() call before every serverAPI - it's so confusing! Witout noStore() dev server works absolutely fine, but when I run npm 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 pass initialTweets to client component, and there's no .query() in await serverClient.tweet.infiniteFeed({}). I would prefer this method, but when I run npm 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

Husky answered 26/1 at 17:43 Comment(4)
Did you ever get this sorted? Exactly the same issue I'm facing, and I share the same sentiment — all client side examples!Disforest
Update on last comment - I reviewed this video youtu.be/G2ZzmgShHgQ?t=296 and it's explained very well.Disforest
Same problem here. Have not found a satisfactory answer yet. What is the use of tRPC if we cannot use node functionality? I could not find a single example that made use of node libraries (a simple http request for example) inside the trpc procedure, so I'm guessing it is not possible at all?Lukewarm
same problem. any news?Tripetalous

© 2022 - 2024 — McMap. All rights reserved.