Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Next 13] Server Component + Layout.tsx - Can't access the URL / Pathname #43704

Closed
adileo opened this issue Dec 4, 2022 · 176 comments
Closed

[Next 13] Server Component + Layout.tsx - Can't access the URL / Pathname #43704

adileo opened this issue Dec 4, 2022 · 176 comments
Labels
area: app App directory (appDir: true) bug Issue was opened via the bug report template. linear: next Confirmed issue that is tracked by the Next.js team. locked Navigation Related to Next.js linking (e.g., <Link>) and navigation.

Comments

@adileo
Copy link

adileo commented Dec 4, 2022

Edit: Please see the response and solutions here: #43704 (comment)

@adileo adileo added the bug Issue was opened via the bug report template. label Dec 4, 2022
@adileo
Copy link
Author

adileo commented Dec 4, 2022

Related comment:
#41745

@Fredkiss3
Copy link
Contributor

Since layouts can wrap multiple routes and don’t rerender on route changes, it cannot take these arguments as props.
for the use cases you are looking for, NextJs. Provides some hooks that you can use inside children client components in the layout.

If you want to highlight a selected link, you can use useSelectedLayoutSegment, in the docs : https://beta.nextjs.org/docs/api-reference/use-selected-layout-segment

The useSelectedLayoutSegment hook allows you to read the active route segment one level down from a layout. It is useful for navigation UI such as tabs that change style depending on which segment is active

to get the params, you would have to use it either in the page.tsx server component, or by using usePathname inside a child client component of the Layout.

The question about the utility of a layout, I see this file as for ex a component that wraps your dashboard and first check if you are logged in, (with cookies and all) and can redirect to login if it is the case. As well as provide a shell witch contain a sidebar (for ex), and that sidebar is client component which uses useSelectedLayoutSegment to determine which link should be highlighted.

@adileo
Copy link
Author

adileo commented Dec 5, 2022

Thanks a lot for you Answer @Fredkiss3
At the end I resorted to do something like you described but wasn't very sure on the approach. Now at least I'm sure it's the only way to go.

The question about the utility of a layout, I see this file as for ex a component that wraps your dashboard and first check if you are logged in, (with cookies and all) and can redirect to login if it is the case. As well as provide a shell witch contain a sidebar (for ex), and that sidebar is client component which uses useSelectedLayoutSegment to determine which link should be highlighted.

In this specific case I think neither Layouts are useful, for example if you want to redirect the user to the login page and you want to preserve the previously accessed URL, to redirect the user back once logged id, you can't do that, since you can't access the current page URL.
At the end I resorted to "middleware.tsx" in order to do that.

At this point I'm thinking layouts just as dummy "html" wrappers easily replaceable by a page.tsx splitted with different components. The benefits of not having to reload/re-render the layout seems trivial against the limitations/boilerplate needed to make them work. But open to change idea on that :)

@Fredkiss3
Copy link
Contributor

Fredkiss3 commented Dec 5, 2022

The thing is that if you use a page.tsx with multiple component (like a Layout component that wraps your entire page), with each page navigation you have (1) a rerender, (2) you loose the benefit of nested layouts since your page will need to have logic for each level of nesting. Instead of that you could delegate the basic layout of your page to a higher layout component and simplify your page logic, which will contains only the logic for the concerned page.

With a page component of a blog with a sidebar with all articles you have :

 // app/blog/[slug]/page.tsx

export default async function ArticlePage({params}) {
  const sidebarArticles = await getAllArticlesForSidebar();
  const currentArticle = await getArticle(params.slug);

  return  (
    <RootLayout>
          <BlogLayout sidebarArticles={sidebarArticles}>
             <article>
                {/* page content ... */}
             </article>
          </BlogLayout>
    </RootLayout>
}

But with layouts, you can put the logic of the sidebar up in the hierarchy :

 // app/blog/[slug]/page.tsx

export default async function ArticlePage({params}) {
  const currentArticle = await getArticle(params.slug);

  return  (
    <article>
       {/* page content ... */}
    </article>
}

 // app/blog/layout.tsx

export default async function BlogLayout({children}) {
  const sidebarArticles = await getAllArticlesForSidebar();

  return  (
     <main>
            <SideBar articles={sidebarArticles} />
            {/* this will be the content of your ArticlePage component for ex. */}
            {children} 
     </main>
}

// app/layout.tsx

export default async function RootLayout({children}) {
  return  (
     <html>
            <head>
               <link rel="icon" href="/favicon.svg" />
                <meta name="viewport" content="width=device-width" />
           </head>
            <body>
              {children}
           </body> 
     </main>
}

The Layout will be used by every child of the layout which wraps them and you could nest as many layouts as you need.
One other benefit is that, on the server when navigating inside a layout, only the data needed for the children will be refetched (unless forced), but with the first approach, each time you navigate to another route, you have to make multiple requests to fetch your data.

@balazsorban44 balazsorban44 added the area: app App directory (appDir: true) label Dec 6, 2022
@jleclanche
Copy link
Contributor

It's extremely counter-intuitive that there's basically no way to get the pathName from a server-side component. usePathname should at the very least be callable from the server-side as well even if it hits a different code path.

@eric-burel
Copy link
Contributor

eric-burel commented Jan 13, 2023

I've been exploring Next.js codebase for an article about server context. In order to get access to more request information than headers and cookies, as far as I understand, a small modification would be needed in Next.js code so that the underlying AsyncLocalStorage includes the request URL in addition to cookies and headers. This way you could create a "url()" function.
See this file: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/run-with-request-async-storage.ts
Edit: up to date file: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/async-storage/request-async-storage-wrapper.ts

Not sure how this would interact with layouts, that's the generic logic that populates the "cookies()" and "headers()" functions basically

I am not sure why this function haven't been implemented yet, maybe there is a good reason for that

@pauliusuza
Copy link

We would like to access the current url / path as is in the Layout in order to set specific css variables on the body element

@irfanullahjan
Copy link

At least for the redirect() e.g. when a user is not logged in, a workaround would be to place it in the layout.js of each "top-level" route segment. It worked nicely for me.

To redirect to login:

// app/some-private-segment/layout.js

import { redirect } from "next/navigation";
import { myFetchWrapper } from "../../utils/myFetchWrapper";

export default async function PrivateLayout(props: any) {
  const user = await myFetchWrapper("/auth/current-user");
  if (!user?.userId) {
    redirect("/user/login");
  }
  // ...
}

To redirect away from login and signup pages

// app/user/layout.js

import { redirect } from "next/navigation";
import { myFetchWrapper } from "../../utils/myFetchWrapper";

export default async function LoginSignupLayout(props: any) {
  const user = await myFetchWrapper("/auth/current-user");
  if (user?.userId) {
    redirect("/");
  }
  // ...
}

Back to the original question: the idea of URL and the request object gets a bit confusing due to nested layouts. When the user navigates between pages deeply nested, the higher-level layouts don't necessarily get recomputed (as far as I understand) and all that caching behavior makes this even more difficult to understand what really goes on during a request. But still, maybe some of the request context could be passed as read-only to the server components as props.

We also have the option to use a middleware for this anyway.

@karlhorky
Copy link
Contributor

karlhorky commented Jan 23, 2023

I'm also coming here trying to access searchParams from a layout in order to do /login redirection.

Why not Middlewares? I guess often Next.js Middlewares are suggested as a solution for auth redirection, but middlewares don't currently support a Node.js runtime, so they are not useful for cases such as querying a database directly.

Another way of performing a /login redirect using searchParams with lack of props passed to Layout is to:

  1. Duplicate your logic for detecting a necessary redirect from login/page.tsx to login/layout.tsx
  2. If your logic in login/layout.tsx detects that a redirect is necessary, only return props.children in the Layout and handle the redirect from login/page.tsx

For example:

// login/layout.tsx
import { cookies } from 'next/headers';

export default async function AuthLayout({ children }: { children: React.ReactNode }) {
  const sessionToken = cookies().get('sessionToken')?.value;

  if (sessionToken) {
    // Avoid rendering layout if user already logged in
    if (await getUserByToken(sessionToken)) return children;
  }

This return children will cause the page to be rendered, which in turn will redirect the user based on the ?returnTo= value in the search parameters:

// login/page.tsx
import { cookies } from 'next/headers';

export default async function LoginPage({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined };
}) {
  const sessionToken = cookies().get('sessionToken')?.value;

  if (sessionToken) {
    // Redirect to `returnTo` search param if user already logged in
    if (await getUserByToken(sessionToken)) {
      redirect(searchParams.returnTo || '/');
    }

    // If a user is not found, delete the invalid session token
    cookies().delete('sessionToken');
  }

@karlhorky
Copy link
Contributor

@adileo would you be open to editing this issue to add some further details? For example:

  1. Editing your title + description of this PR to also explicitly mention searchParams? The pathname by itself wouldn't contain that
  2. Adding a StackBlitz demo - @mpereira created a nice demo in RootLayout doesn't receive searchParams as props #43863 which could possibly be used as a starting point

After these changes I think it's possible to close #43863 as a duplicate...

@pauliusuza
Copy link

For anyone looking for a workaround of accessing current url on the serverside layout (appDir: true), here's how we did it using a middleware:

// /middleware.ts
import { NextResponse } from 'next/server';

export function middleware(request: Request) {

  // Store current request url in a custom header, which you can read later
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-url', request.url);

  return NextResponse.next({
    request: {
      // Apply new request headers
      headers: requestHeaders,
    }
  });
}

Then use it from inside of a root layout:

// /app/layout.tsx
import { headers } from 'next/headers';

export default function RootLayout() {
  const headersList = headers();
  // read the custom x-url header
  const header_url = headersList.get('x-url') || "";
}

@y-nk
Copy link

y-nk commented Feb 3, 2023

I've been trying @pauliusuza workaround which worked seamlessly BUT have spent a day to discover that it'd disable SSG. if you put headers in your root layout, there's not a single page which will be able to SSG ever ⚠️

Full explanation here:

#41745 (comment)

TLDR:

@balazsorban44
Copy link
Member

Hi, thanks for opening this issue, we recently upgraded the documentation to explain this behavior a bit better.

Check out the following link (there is also a "Detailed explanation" tab):
https://beta.nextjs.org/docs/api-reference/file-conventions/layout#good-to-know

@balazsorban44 balazsorban44 added the Navigation Related to Next.js linking (e.g., <Link>) and navigation. label Feb 21, 2023
@y-nk
Copy link

y-nk commented Feb 21, 2023

@balazsorban44

Unlike Pages, Layout components do not receive the searchParams prop. This is because a shared layout is not re-rendered during navigation which could lead to stale searchParams between navigations.

Does that mean the template.tsx file will receive the searchParams?

@karlhorky
Copy link
Contributor

karlhorky commented Feb 21, 2023

Hi, thanks for opening this issue, we recently upgraded the documentation to explain this behavior a bit better.

Check out the following link (there is also a "Detailed explanation" tab): beta.nextjs.org/docs/api-reference/file-conventions/layout#good-to-know

@balazsorban44 I think these docs only relate to the searchParams prop, right? Eg. not the title of this issue, which is about getting access to the URL and pathname in a Server Component or layout:

[Next 13] Server Component + Layout.tsx - Can't access the URL / Pathname

@destino92
Copy link

I am also stuck with this trying to access the current path from a server component and can't seem to find a solution around. I can use it in a client component but it is undefined on some first render

@drewlustro
Copy link

I share the same problem and have a slightly tangential question for devs on this thread:

When working with Next 13 app beta, how do you discover type & prop definitions for Next.js? I'm looking for a technique that is more efficient than browsing every single d.ts file in node_modules/next/...

To my knowledge, Next.js docs does not publish a master list of types.

Until I discovered that this is a bug and/or simply unsupported, I was guessing prop types, hoping that VSCode would autocomplete... ex: LayoutProps LayoutContext PageProps PageContext APIRouteContext

@Fredkiss3
Copy link
Contributor

@drewlustro , with next 13, you basically have to type your page yourself. Since the types are rather simple I don't think this is a big issue, but if you want to see which types to use for your pages & layouts you could run next dev and see the generated folder for types at .next/types or else you could run a build and next would error if your types are incorrects. With the plugin you could even have warnings in the editor directly.

Maybe in the future they will publish, or maybe not since with the upcoming advanced routing patterns, it will be difficult to ship a generic type for Layouts and Pages for parallel routes for ex.

@Fredkiss3
Copy link
Contributor

Fredkiss3 commented Feb 26, 2023

@drewlustro But if you want types for layout and other, I inspected myself the generated types by NextJs and created some custom types that I use for my project :

// src/next-types.d.ts
type PageParams = Record<string, string>;
export type PageProps<
    TParams extends PageParams = {},
    TSearchParams extends any = Record<string, string | undefined>
> = {
    params: TParams;
    searchParams?: TSearchParams;
};

export type LayoutProps<TParams extends PageParams = {}> = {
    children: React.ReactNode;
    params: TParams;
};

export type MetadataParams<
    TParams extends PageParams = {},
    TSearchParams extends any = Record<string, string | undefined>
> = {
    params: TParams;
    searchParams?: TSearchParams;
};

export type ErrorBoundaryProps = {
    error: Error;
    reset: () => void;
};

@drewlustro
Copy link

@Fredkiss3 Thanks!

@karlhorky
Copy link
Contributor

karlhorky commented Mar 4, 2023

For anyone looking for a workaround of accessing current url on the serverside layout (appDir: true), here's how we did it using a middleware [using a 'x-url' custom header]:

We just implemented something similar to pass the pathname from Middleware to a layout for a restricted area, and then in the layout, redirect to the login with a returnTo query parameter with the location the user tried to access - if the authentication failed eg. if session token is not valid:

Commit: upleveled/next-js-example-winter-2023-atvie@353fa9f

middleware.ts

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const requestHeaders = new Headers(request.headers);

  // Store current request pathname in a custom header
  requestHeaders.set('x-pathname', request.nextUrl.pathname);

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

app/restricted/layout.tsx

import { cookies, headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { getUserBySessionToken } from '../../database/users';

export default async function RestrictedLayout(props: { children: React.ReactNode }) {
  const headersList = headers();
  const cookieStore = cookies();
  const sessionToken = cookieStore.get('sessionToken');

  const user = !sessionToken?.value ? undefined : await getUserBySessionToken(sessionToken.value);

  // Redirect non-authenticated users to custom x-pathname header from Middleware
  if (!user) redirect(`/login?returnTo=${headersList.get('x-pathname')}`);

  return props.children;
}

@kelvinpraises
Copy link

Using middleware breaks Client-side Caching of Rendered Server Components.

@mormahr
Copy link

mormahr commented Mar 7, 2023

@balazsorban44

Unlike Pages, Layout components do not receive the searchParams prop. This is because a shared layout is not re-rendered during navigation which could lead to stale searchParams between navigations.

Does that mean the template.tsx file will receive the searchParams?

I didn‘t find an open feature request for this, but this seems like it‘d solve a lot of use cases. Are you planning to file one? @y-nk

@y-nk
Copy link

y-nk commented Mar 8, 2023

i asked @leerob a while ago and he replied "we don't know yet, let's see"

@xriter
Copy link

xriter commented Mar 9, 2023

So, just to get this clear:
If one renders a header with a menu bar in a server component, you are unable to highlight the currently active menu item because there currently is no way of knowing the current url in a server component? For that you HAVE to use a client component?

I guess PHP isn't that bad after all 😉

@0xbid
Copy link

0xbid commented Feb 19, 2024

so are they not going to fix this?

@ivan-kleshnin
Copy link
Contributor

ivan-kleshnin commented Feb 21, 2024

For anyone looking for a workaround of accessing current url on the serverside layout (appDir: true), here's how we did it using a middleware:
@pauliusuza

This approach with middleware + call of NextResponse.next({request: {headers: requestHeaders}}) is limited to a single middleware. As soon as your app gets more complex, you get middleware stacking and it fails:

export const mdFactory: MiddlewareFactory = (next: NextMiddleware) => {
  return async (req: NextRequest, event: NextFetchEvent) => {
    req = ... // attempt to modify request
    ...
    next(req, event) // this function accepts NextRequest only, does not accept ModifiedRequest
    // `req` is a class instance so you can't clone it either
    ...
  }

We need a proper solution for the issue.

Wait isn't 14 out? Are you saying this is available now?
@0xbid

next-url behaves inconsistently between DEV and PROD modes and is officially discouraged for public use.
See: #57762 (comment) It's an internal thing.

@dark-swordsman
Copy link

dark-swordsman commented Feb 21, 2024

So let me ask this then: If we can't get the URL (at least reliably) to do conditional logic for pages, then how are people getting around this problem?

If I want to have a set of "home" pages at / with their own navbar (which should be done via layout), and then have a /dashboard set of pages with their own navigation (also done by layout), how do I solve this problem?

Do I just give up and manually add the navbar to each of the / pages? Or I guess I can just make a /home directory that would be my traditional home page.

Edit: My apologies. I guess I realized I could just do it on the component itself, so I don't have to screw up the entire layout.

@ouedyan
Copy link

ouedyan commented Feb 22, 2024

So let me ask this then: If we can't get the URL (at least reliably) to do conditional logic for pages, then how are people getting around this problem?

If I want to have a set of "home" pages at / with their own navbar (which should be done via layout), and then have a /dashboard set of pages with their own navigation (also done by layout), how do I solve this problem?

Do I just give up and manually add the navbar to each of the / pages? Or I guess I can just make a /home directory that would be my traditional home page.

Edit: My apologies. I guess I realized I could just do it on the component itself, so I don't have to screw up the entire layout.

@dark-swordsman For this kind of use case just use route groups: https://nextjs.org/docs/app/building-your-application/routing/route-groups and nested layouts if you want to do it properly.

@delbaoliveira
Copy link
Contributor

Hey everyone, thank you for sharing some of the challenges with accessing pathname.

We'd like to share some context into our reasoning for not exposing it in Layouts, and discuss how you can implement the use cases you've shared here.

Here's a quick overview:

  • Why Next.js doesn't expose the request/response objects in Server Components.
  • Why Server Components don't have access to the full pathname.
  • Available APIs and how to implement some of the use cases you've shared.

Why doesn't Next.js expose the request and response objects?

If we take a step back, the question "Why can't I access pathname or current URL?" is part of a bigger question: "Why can't I access the complete request and response objects?"

Next.js is both a static and dynamic rendering framework that splits work into route segments. While exposing the request/response is very powerful, these objects are inherently dynamic and affect the entire route. This limits the framework's ability to implement current (caching and streaming) and future (Partial Prerendering) optimizations.

To address this challenge, we considered exposing the request object and tracking where it's being accessed (e.g. using a proxy). But this would make it harder to track how the methods were being used in your code base, and could lead developers to unintentionally opting into dynamic rendering.

Instead, we exposed specific methods from the Web Request API, unifying and optimizing each for usage in different contexts: Components, Server Actions, Route Handlers, and Middleware. These APIs allow the developer to explicitly opt into framework heuristics like dynamic rendering, and makes it easier for Next.js to track usage, breaking the work, and optimizing as much as possible.

For example, when using headers, the framework knows to opt into dynamic rendering to handle the request. Or, in the case of cookies, you can read cookies in the React render context, but only set cookies in a mutation context (e.g. Server Actions and Route Handlers) because cookies cannot be set once streaming starts.

Why don't Server Components have access to the full pathname?

Similarly, pathname is not available in Server Components but is available in Client Components through the usePathname hook. There are couple of reasons for this:

  • Server Components always render on the server, including for subsequent navigations.
    • Next.js splits the rendering work into route segments (layouts and pages), which are Server Components by default. Since segments don't depend on each other, this allows rendering and data fetching to initiate in parallel, and for segments to be cached and streamed independently.
      • Side note: This is related to why you can't pass props from layouts to pages. If props could be passed, it would create dependencies between segments. Similarly, layouts only receive params above them, but are independent from its children.
    • To reduce the rendering work and RSC payload size between navigations, Next.js doesn't re-render shared layouts (and in the future, will also cache segments for PPR). So any code that depends on pathname, which changes between navigation, would become stale.
    • To prevent pathname from becoming stale, you'd need to refetch and render the whole route on the server on every navigation - increasing server resources, and losing the benefits of partial rendering. Or, we'd have to cache all the possible variants of a layout.
    • But, if we know you don't use pathname, we can cache the segments as static HTML. In the future, when we ship Partial Prerendering, this will also allow us to go even more granular than the pages/layout level. We can split the static/dynamic parts at the Suspense level.
  • Client-side hooks that observe the URL rerender on navigation, making them a good candidate for logic that depends on URL changes.
    • Client Components are prerendered on the server, so the initial HTML will contain the correct state.
    • Client components are downloaded once and not refetched on navigation.
    • Changes in Client Components can happen instantly, before a request/response lifecycle completes.

Considering all the above, we still want you to be able to achieve your goals. Let's look at what APIs are available in Next.js that allow you to access the URL information, and when you might use them.

APIs and Examples

Client-side Hooks

In Next.js, Components are prerendered on the server for the initial request, using these APIs is a valid part of the model and not a de-optimization.

API Usage
usePathname Read the current pathname
useSearchParams Read the current query params.
useParams Read the dynamic params in the current URL
useSelectedLayoutSegments Read the active route segments (children) below the Layout it is called from.

Examples

  • Active Links
    • usePathname or useSelectLayoutSegment can be used to conditionally style links based on the current segment (Docs, Demo, Code).

Server-side APIs

APIs Where Usage
params page.js component, layout.js component (params above it), generateMetadata Statically or dynamically render a page on the server based on dynamic params.
searchParams page.js component, generateMetadata Render a page on the server based on the search params of the incoming request. Client navigations that update search params will send a new request to the server and cause a re-render.
Parallel Routes - Render one or more pages within the same layout simultaneously. The terminating segments (page.js) receive complete params and searchParams and can, therefore, be used for server-side UI that depends on the URL, such as breadcrumbs.

Examples

  • Dynamically setting Metadata
    • Use generateMetadata and derive from params or searchParams (Docs).
    • Setting locales - by following the recommendations for i18n, you should have access to the locale params in generateMetadata (Docs).
    • Setting canonical URLs - derive from params or searchParams (e.g. if you need unique URLs for sorting, filtering, pagination) in generateMetadata (Docs).
  • Breadcrumbs
    • Use Parallel Routes for breadcrumbs that rely on external data (Docs, Demo, Code).
  • Handling auth redirects
  • Sorting, Filtering, and Pagination / Fetching data based on search params
    • Use searchParams prop inside pages (Docs, Demo, Code)
    • Can use Parallel routes for shared UI. E.g. sidebar
  • Rendering layouts for specific pages
    • Use Route Groups to group pages that share a layout (Docs).

Since the RSC model is relatively new, it'll take some time for new patterns to emerge. I'll update this comment as docs and examples are added.

If you're trying to implement a pattern not covered above, or are unsure how to use these APIs for your use case, please let us know by opening a discussion. Also, feel free to contribute patterns that have worked for you (🙏).

@vordgi
Copy link
Contributor

vordgi commented May 2, 2024

Hello @delbaoliveira! First of all - thank you for the detailed explanation of the team's position towards this thread. Nevertheless, I still have questions, and the current functionality is still not enough. To avoid writing a long answer here, I described them in a discussion.

@tozz
Copy link

tozz commented May 2, 2024

And if someone ever wanted an example of how to over engineer something (complexity over simplicity) this would be one of the best examples I've seen :)
It was somewhat clear before but even more so now, moving all our projects off Next was/is the right choice. I guess you don't want that pattern added to the list though 😅

@landsman
Copy link
Contributor

landsman commented May 5, 2024

Let's vote: #65385

@XoL1507
Copy link

XoL1507 commented May 5, 2024

can we make it clear? tell me my what is the problem of vercel

@h3rmanj
Copy link

h3rmanj commented May 6, 2024

Thank you for the detailed response @delbaoliveira! I'm trying to solve the auth-redirect stuff, and going the middleware route seems like it would work. The documentation also seems to have been worked on a lot since last I read, which is very much appreciated.

Going the middleware route still seems a little off though. I think the reason I reached for layouts initially is that it integrates with the routing system. Having a app/(authenticated)/layout.tsx that redirects on non-logged-in users, and being able to fill that route group with routes that should only be visible to authenticated users seems like a natural solution.

To do the same with a middleware, I need to basically re-implement the routing system in a regex, to ensure it runs on the correct paths. I'd also need to remember to update it with new routes, which seems like it could be easy to forget. There's also the limitation that you can only have one middleware. What if I needed to add another middleware for a different set of routes, for another feature/use case? Then I couldn't use the matcher config anymore, and have to manually re-implement that too to conditionally run each middleware my application needs.

Like last time, the response still doesn't answer why headers() and cookies() exist, while pathname() does not. If I opt into dynamic rendering for my layout, what is so different about the pathname, from headers or cookies? All the reasons you list for pathname not being accessible, seems like they would apply to headers and cookies too.

@crawlchange
Copy link

crawlchange commented May 7, 2024

Hi,

I am facing the exact same issue.

I'm using static exports and I just want to wrap all my pages in a HOC.

This HOC will add a header and render it differently for each page (highlighting the current page in the nav menu).

This 100% works with static exports IF I go to each and every page and do the wrapping myself.

This is ridiculous though, this is why we use frameworks.

Now, a NextJS template supposedly exists to do exactly that: to wrap every page.

Unlike the layout, the template does not persist state; it is different for every page.

The template receives the "children" prop. The "children" prop is obtained, by NextJS, based on the current route, ON THE SERVER SIDE.

So, there is NO excuse for the template to not receive a "currentRoute" prop together with the "children" prop.

I see this issue has been opened for a year, and no real solutions given by the NextJS team.

This is crazy. We need a simple way to wrap every page in a route-aware HOC, like you can do in any other frontend or backend framework in existence.

Note: This is asking much less than to have access to the "request" or to the "headers". Why can't the template just know what page is being rendered? It DOES know the entire jsx of the page (via the "children" prop) so it is ridiculous that it cannot get the name of the page!

The "url" is a client-side concept, the "route" is a server-side concept. The entire purpose of a routing system is to convert a url into a route, or "page". It is absurd that the template cannot know what is the name, path, or whatever, of the page that is currently being rendered inside of it.

In other words, what I'm saying is that whatever method in the NextJS codebase that takes the template and wraps the page in it, already knows what the page is, cause it's giving the template it's entire jsx, so it just has to additionally hand over to the template the path of that page (the "route")!

@crawlchange
Copy link

This, taken from the docs, is a simplified description of what NextJS does behind the scenes:

Screenshot from 2024-05-07 13-44-08

My proposal is: replace

<Template key={routeParam}>{children}</Template>

with

<Template key={routeParam} routeParam={routeParam}>{children}</Template>

so as to allow the developer to access the routeParam in the template. I'm assuming "routeParam" is a string representing the page path in the filesystem.

@xc1427
Copy link

xc1427 commented May 8, 2024

I suppose here is not a recommended approach though https://www.propelauth.com/post/getting-url-in-next-server-components

@markedwards
Copy link

We don't recommend doing auth checks in layouts, instead you can do optimistic checks in Middleware and secure checks in a data access layer. (Docs, Demo, Code)

This is the use-case I'm here for. I need to preserve the originally requested pathname in the redirect that results from an authentication failure (session expired, for example). This is so that the user gets the content they want after they log in.

@delbaoliveira, the sample code doesn't address this relatively common scenario. As an aside, it also does the auth check in a layout (/app/dashboard/layout.tsx -> getUser() -> verifySession() -> redirect()), which you should fix in the demo if that's not the best practice.

In the context of that demo code, in order to pass the originally requested pathname to /login as a param, so it can redirect after successful auth, it needs to be obtained in verifySession(), which is called from a server component. Is there any officially sanctioned way to achieve this?

@vordgi
Copy link
Contributor

vordgi commented May 17, 2024

Okay, my beloved next.js, one more try.

Let's do it! createPageContext PR!

@crawlchange
Copy link

crawlchange commented May 17, 2024

To add to the ridiculousness of the situation, I'm noticing that actual dynamic variables, such as "currentUser", are often used in the layout in working Next.js projects.

Why does the Next.js team think that the filesystem path of the current page being rendered under the layout is a "dynamic variable" to begin with? It only updates when there are router changes (which are similar to complete page refresh, or to switching html file in static exports).

"children", for example, is a dynamic variable (containing jsx!) that is DEPENDENT on the filesystem path of the current page.

After two weeks from my last comment, it stays incomprehensible to me why would they say that layouts cannot access the filesystem path of the current page being rendered under said layout, but even more so for templates, since templates supposedly exist exactly because of layout's limitations.

They keep talking about "getPathname", while "pathname", as a concept, does not even make sense for a backend. Backends do not access the "pathname", they access the route. And, emphasis here, they DO access the route.

@eric-burel
Copy link
Contributor

eric-burel commented May 19, 2024

@delbaoliveira thanks for this write-up above which explains the current situation in details. However, as the other comments are indicating, this is not totally convincing and not answering the questions raised by the community. I'd like to rephrase some of the questions I have based on these explanations.

Sidenote: the topic is sometimes leading to heated discussion, so I'd like to start by stating that I am extremely thankful for the open source work done by Next.js core team and other contributors. We are in the heart of the reactor of what web development may look like in the years to come. This is an exciting thing, not a bad one!

So let's imagine we have a request() function:

  • headers() and cookies() have the same staleness issue as request(), and when used in layouts lead to a huge footgun: checking authorization in layouts as you well noted in the feedback discussion

  • regarding tracking calls to these dynamic helpers, this is solved by the use of an AsyncLocalStorage, the same strategy used for cookies and headers would work for the request, using a function call request() rather than passing an object. I think this pattern has been adopted by the community and seems fine (I only wonder if it works in 3rd party NPM packages or not)

  • the key point here seems to be RSC layouts that do not rerender during navigation. I didn't met a similar approach in the industry so I feel like Next.js is pioneering here. This should be the heart of discussions around access to the request object.

To reduce the rendering work and RSC payload size between navigations, Next.js doesn't re-render shared layouts

Many of us might prefer losing some performance and have a request() function.

"But, if we know you don't use pathname, we can cache the segments as static HTML

I think I don't totally get this sentence. This bothers me because currently we do use the pathname in RSCs by using cache as a server context, as done in server-only-context and shoving params there. Contributors of various i18n projects like next-intl or our own setup at Devographics picked this pattern. As this is phrased, it seems like even props drilling from the page component could be an issue. So pathname is actually used and you can't know about it which seems to be a worst case scenario. Again I probably miss something here.

I've raised the concern that, as to my best knowledge, there is no official guarantee that a page has the same render cycle than the components it contains, similarly to how layouts and pages currently work. This would break this cache as context pattern. I suspect PPR could break this guarantee by allowing dynamic components within static pages.

So here are a few questions:

  • I feel like a missing piece here: Vercel has a deep knowledge of infrastructure, is there a positive infrastructure impact that would explain why layout do not rerender, which in turns explains the lack of a request() utility?

  • Should we consider the cache-based server context as a ticking bomb that will cause trouble when we want to benefit from PPR, or is it guaranteed that a cache call at page level will always precede a render of the RSCs ? Or should we consider it a perfectly valid pattern until something better comes up?

To the community: to be constructive, I think we should work on proposing alternate designs for layouts, PPR and similar features, especially people that have the chance to be knowledgeable both about distributed infrastructure and "backend-for-frontend", if they don't already all work at Vercel :)

@crawlchange
Copy link

@eric-burtel I don't understand, I feel like the relevant points are being ignored.

"the key point here seems to be RSC layouts that do not rerender during navigation. I didn't met a similar approach in the industry so I feel like Next.js is pioneering here. This should be the heart of discussions around access to the request object."

If that is the key point, why are we ignoring that templates DO rerender during navigation, as per the docs? I don't understand how this is still unaddressed after 2 weeks of being brought up.

Again, it is not about the "request object". It is of course bad for any backend to not be able to access a request object, but it is much worse and more fundamental that the backend cannot access it's own FILEPATH.

But let's say for the sake of argument that the issue is merely not being able to access the request object.

Why can't the request object be made accessible to the templates?

But, again, the real question is: why can't the template know what is the path of the jsx that it is wrapping.

@eric-burel
Copy link
Contributor

@crawlchange I think that it's not about the difference between templates and layouts, but about the fact that your component may end up in a layout, even when you don't use one.

So the idea of having pure server-side layouts that do not rerender during client navigation imply that you can't have access to the full request within components that may end up in a layout.

Regarding request object versus accessing it's "own filepath", any routes knows its path, the problem is that the path may be dynamic and include variables that are only known at request time, the dynamic route parameters. So it's indeed a matter of accessing request information, the URL, which in turns needs some thinking about the impact on the app router and layouts that are shared between multiple dynamic URL.

That's why I think that if we want the request() helper that magically solves all our issues, we need a deep thinking about what a layout is in a pure server-side context: are they worth it, what impact they have on performance etc. Also taking PPR into account that kinda impacts this feature.

Some people thinks about switching to Astro because of this but remember that Astro doesn't have a concept of server-rendered layout yet (docs "there is nothing special about a layout component"). Passing params around uses a pattern similar to React cache via the Astro.locals object.

@crawlchange
Copy link

crawlchange commented May 22, 2024

Hey @eric-burel.

When I talk about path, I'm talking about filesystem path. That's the constant confusion that I'm noticing, and that's what I think is severely missing from Next.js' docs.

The filesystem path is not dynamic, and it does not include variables that are only known at request time. The filesystem path is where the file is located.

This is called a route in backend systems.

For example, in Next.js filesystem-based routing, the route of a file that is located in app/users/[id] is /users/[id]. With this I mean the STRING "/users/[id]", NOT the dynamic resolution to "/users/1" or whatever.

"/users/[id]" is a single route, not an infinity of possible routes. This single route does correspond to an infinity of possible urls. The urls are dynamic, the ROUTE is NOT. The role of a routing system is to map urls to routes, extract the dynamic parameters and make them available to application code via some prop.

The issue with Next.js is that the docs completely ignore that routes exist and that developers might need to access the route. When you search for "how to get the route value in Next.js" you are redirected to getPathname in any search engine. The route is a backend concept, the pathname is a frontend concept, so this conflating of the two is weird.

I've seen large threads here where the poor developer just wanted to add a header where he could highlight the current page in the nav. This is not client-side interactivity: the component is only updated on navigation to another page (eg another html file in a static export). Parallel routes and so on is over-engineering this.

The templates do re-render between page navigation, and the route is not dynamic, it is one-to-one correspondent to a file in Next.js: it is going to change if and only if a file is renamed or moved somewhere else. With route here I mean the route of the file that is being wrapped by the template, i.e., the route or filesystem path of the page that is currently being rendered.

So I don't understand why Next.js completely ignores the concept of routes and doesn't make the routes available to templates, or why this keeps being conflated with client-side concepts such as "pathnames" or "requests". The location of a file in the app folder has nothing to do materially with a client-side request.

@crawlchange
Copy link

crawlchange commented May 24, 2024

Man, I see so many repositories out there using 'use client' and then stuff like useEffect on the root layout. Next.js allows that, but doesn't allow a, I don't know, "getFilesystemPath" in a mere template 😐

Copy link
Contributor

github-actions bot commented Jun 7, 2024

This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@github-actions github-actions bot added the locked label Jun 7, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 7, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: app App directory (appDir: true) bug Issue was opened via the bug report template. linear: next Confirmed issue that is tracked by the Next.js team. locked Navigation Related to Next.js linking (e.g., <Link>) and navigation.
Projects
None yet
Development

No branches or pull requests