t3-oss / create-t3-app

The best way to start a full-stack, typesafe Next.js app
https://create.t3.gg
MIT License
25.44k stars 1.17k forks source link

feat: Full Route Cache as a default #1663

Closed ThomasBouasli closed 7 months ago

ThomasBouasli commented 11 months ago

Is your feature request related to a problem? Please describe.

Since TRPCReactProvider is being render in the root layout and has cookies passing down to it, it counts as an dynamic function and opts out of the Full Route Cache.

Describe the solution you'd like to see

A way for the default Full Route Cache to be used and have TRPCProvider as a top level provider, I don't know if this is possible though.

Describe alternate solutions

Maybe address that as a comment so it's clearer? I had trouble figuring out why my static pages weren't actually static.

Additional information

No response

ArjobanSingh commented 11 months ago

I also faced this exact issue, as my static pages were not working and it took me some time to figure out the TRPCProvider wrapped around the whole app was the culprit, as headers() are passed from there to client component.

dBianchii commented 11 months ago

+1.

This has been bothering me for quite a long time. I really don't know if it is possible, but would love it if it was

dBianchii commented 11 months ago

I found that the only way to do this is to use

export const dynamic = "force-static"

image

But it sucks having to do it like this

ThomasBouasli commented 11 months ago

@dBianchii I don't know how I didn't thought of this tbh. It's bad but better than ditching TRPC. One thing that I'm curious about is how partial pre-rendering will affect this, the content will load faster than the layout, which will probably look weird.

dBianchii commented 11 months ago

@dBianchii I don't know how I didn't thought of this tbh. It's bad but better than ditching TRPC. One thing that I'm curious about is how partial pre-rendering will affect this, the content will load faster than the layout, which will probably look weird.

It does not work with PPR. If you go to canary and turn it on, building with next build completely fails.

I think T3 should definitely fix this. I am not sure if it is something that needs to wait TRPC v11

ThomasBouasli commented 11 months ago

That sucks. I do thinks it's more of an TRPC problem than anything else.

raymclee commented 11 months ago

@dBianchii I don't know how I didn't thought of this tbh. It's bad but better than ditching TRPC. One thing that I'm curious about is how partial pre-rendering will affect this, the content will load faster than the layout, which will probably look weird.

It does not work with PPR. If you go to canary and turn it on, building with next build completely fails.

I think T3 should definitely fix this. I am not sure if it is something that needs to wait TRPC v11

I just remove the cookies function from context and not passing it to the Provider. With app router, we can easily call the cookies function in middleware when needed

Also, got PPR working with unstable_noStore function.

image
ThomasBouasli commented 11 months ago

@raymclee Can you share what you did in your middleware? I don't get how you're passing cookies to the TRPC route with this method.

ArjobanSingh commented 11 months ago

I eliminated the cookies function entirely from the root layout. I believe it's unnecessary in that context since its sole purpose was to be passed to the client component, which instantiates the trpcClient for React, and then we pass it as headers. If we look closely, it becomes evident that we don't actually need these headers to be transmitted from unstable_httpBatchStreamLink. In every browser-to-server request, all the required headers, including the HTTP cookies, are automatically transmitted from the browser. Therefore, if we only need to pass some additional headers, we can simply use new Headers() and pass them, as demonstrated in the following manner.

// react.tsx file

// in trpcClient instantiation using clientApi.createClient

unstable_httpBatchStreamLink({
  url: getUrl(),
  headers() {
    // const heads = new Map(props.headers); REMOVED THIS!
    const heads = new Map(new Headers());
    heads.set("x-trpc-source", "react"); // setting some custom headers
    return Object.fromEntries(heads);
  },
})

However I could be completely wrong about my assumption of the usage of headers() in this unstable_httpBatchStreamLink function, and maybe that headers() were serving something which I am missing. But this workaround works for me with proper auth.

danieljpgo commented 11 months ago

I did the same a few days ago; I didn't send the cookies to the provider through next/headers because this causes the page to opt-in to SSR. Instead, I passed window.document.cookie directly.

You can also opt-out of SSR by using export const dynamic = "force-static" on each page or in the layout. However, removing the import { cookies } from "next/headers" does the trick as well.

Also, I encountered problems using tRPC within generateStaticParams(). I needed to create another createTRPCProxyClient() that does not use cookies in createContext(). Even though the request does not require authentication, if it tries to read cookies within createTRPCContext(), it does not work and throws:

Invariant: Method expects to have requestAsyncStorage, none available

To simplify, I divided my procedures/appRouter into public and authenticated. This way, I can use the public ones freely on static pages.

raymclee commented 11 months ago

@raymclee Can you share what you did in your middleware? I don't get how you're passing cookies to the TRPC route with this method.

like this

const isAuthed = t.middleware(async (opts) => {
  const { ctx } = opts;
  const cookie = cookies().get("token")?.value;
  const session = await getSession(cookie);
  if (!session) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  return opts.next({ ctx: { session} });
});

I think you could even call it inside procedure

debatz commented 11 months ago

I did the same a few days ago; I didn't send the cookies to the provider through next/headers because this causes the page to opt-in to SSR. Instead, I passed window.document.cookie directly.

You can also opt-out of SSR by using export const dynamic = "force-static" on each page or in the layout. However, removing the import { cookies } from "next/headers" does the trick as well.

Also, I encountered problems using tRPC within generateStaticParams(). I needed to create another createTRPCProxyClient() that does not use cookies in createContext(). Even though the request does not require authentication, if it tries to read cookies within createTRPCContext(), it does not work and throws:

Invariant: Method expects to have requestAsyncStorage, none available

To simplify, I divided my procedures/appRouter into public and authenticated. This way, I can use the public ones freely on static pages.

This sounds like a great solution. Could you share an example with us?

dBianchii commented 11 months ago

@juliusmarminge @c-ehrlich , do you have any opinion on these issues?

danieljpgo commented 11 months ago

I did the same a few days ago; I didn't send the cookies to the provider through next/headers because this causes the page to opt-in to SSR. Instead, I passed window.document.cookie directly. You can also opt-out of SSR by using export const dynamic = "force-static" on each page or in the layout. However, removing the import { cookies } from "next/headers" does the trick as well. Also, I encountered problems using tRPC within generateStaticParams(). I needed to create another createTRPCProxyClient() that does not use cookies in createContext(). Even though the request does not require authentication, if it tries to read cookies within createTRPCContext(), it does not work and throws:

Invariant: Method expects to have requestAsyncStorage, none available

To simplify, I divided my procedures/appRouter into public and authenticated. This way, I can use the public ones freely on static pages.

This sounds like a great solution. Could you share an example with us?


// server/api/trpc.ts
export async function createTRPCContext(
  opts: { headers: Headers },
  args?: { unauthenticated: boolean },
) {
  if (args?.unauthenticated) {
    return { db, ...opts, session: null };
  }

  const cookieStore = cookies();
  const session = await getSession(cookieStore);
  return { db, ...opts, session };
}

// server/api/root.ts
export const appRouter = router({...});

export const publicRouter = router({...});

// lib/trpc/server.ts
const createContext = cache(() => {
  return createTRPCContext({
    headers: new Headers({
      cookie: cookies().toString(),
      "x-trpc-source": "rsc",
    }),
  });
});

const createPublicContext = cache(() => {
  return createTRPCContext(
    { headers: new Headers({ cookie: "", "x-trpc-source": "rsc" }) },
    { unauthenticated: true },
  );
});

const serverApp = createTRPCProxyClient<AppRouter>({
  transformer: superjson,
  links: [
    ...,
    () =>
      ({ op }) =>
        observable((observer) => {
          createContext() // authenticated context
            .then((ctx) => {
              return callProcedure({
                procedures: appRouter._def.procedures, // authenticated procedures
                ...
              });
            }),
            ...
        }),
  ],
});

const serverPublic = createTRPCProxyClient<PublicRouter>({
  transformer: superjson,
  links: [
    ...,
    () =>
      ({ op }) =>
        observable((observer) => {
          createPublicContext() // public context
            .then((ctx) => {
              return callProcedure({
                procedures: publicRouter._def.procedures, // public procedures
                ...
              });
            })
            ...
        }),
  ],
});

export const server = {
  app: serverApp,
  public: serverPublic,
};

// app/post/page.tsx
const post = await server.app.post.get.query(); // authenticated
const info = await server.public.info.get.query(); // public

I think I can still refine the implementation, but it works very well for those who want to separate procedures into public and authenticated ones.

I believe that @raymclee solution is also good, moving access to cookies only to the middleware, you can skip all the steps I took and maybe have just one appRouter, I think it can simplify the implementation, but I don't know if there is any disadvantage in this approach.

debatz commented 11 months ago

Thank you @danieljpgo

I was interested in this thread based on principle...

...but I just got derailed on a side-project because it's impossible to create a dynamic route and use generateStaticParams()

As reported by @danieljpgo :

 ⨯ Failed to generate static paths for /[dashboard]:
Error [TRPCClientError]: Invariant: cookies() expects to have requestAsyncStorage, none available.
    at TRPCClientError.from (webpack-internal:///(rsc)/./node_modules/@trpc/client/dist/TRPCClientError-0de4d231.mjs:41:16)
    at eval (webpack-internal:///(rsc)/./node_modules/@trpc/client/dist/index.mjs:69:85)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  type: 'TRPCClientError'
}
andrewdoro commented 10 months ago

This is the issue preventing static generation https://github.com/vercel/next.js/issues/60009

juliusmarminge commented 10 months ago

I eliminated the cookies function entirely from the root layout. I believe it's unnecessary in that context since its sole purpose was to be passed to the client component, which instantiates the trpcClient for React, and then we pass it as headers. If we look closely, it becomes evident that we don't actually need these headers to be transmitted from unstable_httpBatchStreamLink. In every browser-to-server request, all the required headers, including the HTTP cookies, are automatically transmitted from the browser. Therefore, if we only need to pass some additional headers, we can simply use new Headers() and pass them, as demonstrated in the following manner.

This is not the case when the client components are SSRed, then we need the headers passed from the server component. This should be possible using a Promise, which will never be consumed if you don't fetch anything on that page. I filed the above issue as I want to get this working too...

statusunknown418 commented 9 months ago

are there any updates or solutions to this issue? btw:

@dBianchii I don't know how I didn't thought of this tbh. It's bad but better than ditching TRPC. One thing that I'm curious about is how partial pre-rendering will affect this, the content will load faster than the layout, which will probably look weird.

It does not work with PPR. If you go to canary and turn it on, building with next build completely fails. I think T3 should definitely fix this. I am not sure if it is something that needs to wait TRPC v11

I just remove the cookies function from context and not passing it to the Provider. With app router, we can easily call the cookies function in middleware when needed

Also, got PPR working with unstable_noStore function. image

even if I remove the cookies function from the layout it still shows the same error and all pages become SSR, do you maybe have your code for layout.tsx and trpc/react.tsx @raymclee ?

markomitranic commented 9 months ago

I just now realized that simple deletion of trpc/server.ts resolves this problem entirely. If you don't use trpc within server components, and instead treat it as a client-only method of communication, no server context is needed.

Am I missing something here?