next.js icon indicating copy to clipboard operation
next.js copied to clipboard

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

Open adileo opened this issue 2 years ago • 152 comments

Verify canary release

  • [X] I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System: Platform: linux Arch: x64 Binaries: Node: 19.0.1 npm: 8.19.2 Yarn: 1.22.17 pnpm: N/A Relevant packages: next: 13.0.6 eslint-config-next: 13.0.3 react: 18.2.0 react-dom: 18.2.0

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true)

Link to reproduction - Issues with a link to complete (but minimal) reproduction code will be addressed faster

To Reproduce

Not Applicable

Describe the Bug

In the new /app directory within a layout.tsx or Server (Page) Component is not possible to access essential informations like:

  • Current page URL/Pathname: this is especially useful within Layout.tsx if you want for example to be able to highlight the current menu item the user is browsing, or you want to perform some redirect logics based on the page URL
  • Current page Params are not accessible from a parent Layout: eg. /app/{param1}/abc/{param2} the layout positioned within /app/layout.tsx can't read {param1} and {param2}

Expected Behavior

Right now without this kind of functionality the layout.tsx purpose is not clear and it looks almost useless in any kind of real world scenario. Something like "useRouter()" within the server components would be awesome..

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

NEXT-1349

adileo avatar Dec 04 '22 20:12 adileo

Related comment: https://github.com/vercel/next.js/discussions/41745?sort=top#discussioncomment-4077917

adileo avatar Dec 04 '22 20:12 adileo

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.

Fredkiss3 avatar Dec 05 '22 01:12 Fredkiss3

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 :)

adileo avatar Dec 05 '22 14:12 adileo

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.

Fredkiss3 avatar Dec 05 '22 15:12 Fredkiss3

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.

jleclanche avatar Dec 16 '22 18:12 jleclanche

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

eric-burel avatar Jan 13 '23 16:01 eric-burel

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

pauliusuza avatar Jan 20 '23 08:01 pauliusuza

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.

irfanullahjan avatar Jan 22 '23 12:01 irfanullahjan

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 avatar Jan 23 '23 17:01 karlhorky

@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 #43863 which could possibly be used as a starting point

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

karlhorky avatar Jan 23 '23 17:01 karlhorky

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') || "";
}

pauliusuza avatar Jan 31 '23 23:01 pauliusuza

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:

https://github.com/vercel/next.js/discussions/41745#discussioncomment-4858645

TLDR:

y-nk avatar Feb 03 '23 10:02 y-nk

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 avatar Feb 21 '23 12:02 balazsorban44

@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?

y-nk avatar Feb 21 '23 12:02 y-nk

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

karlhorky avatar Feb 21 '23 12:02 karlhorky

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

destino92 avatar Feb 23 '23 19:02 destino92

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

drewlustro avatar Feb 26 '23 17:02 drewlustro

@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 avatar Feb 26 '23 18:02 Fredkiss3

@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;
};

Fredkiss3 avatar Feb 26 '23 18:02 Fredkiss3

@Fredkiss3 Thanks!

drewlustro avatar Feb 26 '23 23:02 drewlustro

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: https://github.com/upleveled/next-js-example-winter-2023-vienna-austria/commit/353fa9f5d6a31a5b283a959aec1edad24f4b4717

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;
}

karlhorky avatar Mar 04 '23 16:03 karlhorky

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

kelvinpraises avatar Mar 05 '23 21:03 kelvinpraises

@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

mormahr avatar Mar 07 '23 22:03 mormahr

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

y-nk avatar Mar 08 '23 00:03 y-nk

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 😉

xriter avatar Mar 09 '23 23:03 xriter

Need to access pathname in server component and layout. Any solution found please?

subhasish-smiles avatar Mar 13 '23 08:03 subhasish-smiles

@karlhorky N.B. The usage of headers() will opt out of static generation/caching.

joshuabaker avatar Mar 17 '23 17:03 joshuabaker

This is much needed feature NextJS team. I definitely have a use case for searchParams in a server side layout.

Edit: @karlhorky my use-case is that I need to fetch data from an API using search params to create my query args and then set classes/meta data within the HTML based on the resulting API response that are applicable to all pages within the layout.

benweiser avatar Mar 17 '23 22:03 benweiser

@benweiser can you edit your post above to provide a detailed account of that particular use case? This may be taken into consideration when the team considers building this feature.

karlhorky avatar Mar 18 '23 07:03 karlhorky

@benweiser can you edit your post above to provide a detailed account of that particular use case? This may be taken into consideration when the team considers building this feature.

Just to add on this, not supporting access to the full request during dynamic rendering in layouts defeats the purpose of layouts, as it may involve reproducing the same logic to each and every page down the stream. And not allowing it in pages would be even more problematic. Use case can be any situation where you prefer a search parameter to a route parameter, for instance when you have many parameters to take into account. An e-commerce search page is typical of this. Though I understand the issue that during client-navigation, supporting such params is tricky.

More broadly, I tend to advocate for a request-centric approach, even during static rendering, as SSG can be seen as precomputing the result of a bunch of requests, eventhough existing frameworks simplify this to focus only on the URL route parameters. Middleware rewrites thankfully make it possible to go beyond the URL, by injecting relevant parts of the request into a "fake" route param then used for rendering, but this is convoluted.

Client-side, this could translate as checking if the layout need a refresh anytime something that would have altered the initial server render (so, the request) changes. Namely, any url change, so not only route params but also search params, and any cookie change too

To sum it up my opinion: let us access the full request, it's up to us devs to invent the use cases 🚀

( I am conscious it's easier said than done though)

eric-burel avatar Mar 19 '23 12:03 eric-burel