Next.js 13 get current URL path in Server Component

This may be due to the current beta-phase of Next.js 13 app directory, but I am struggling to make a simple nav link with an active state based on the current path name–only with server components. I assumed that there were an API to get the current pathname, just like you can get headers and cookies in Server Components. But alas, I couldn't find any. I know this is possible to do with Client Components. But I wan't to keep the nav and its links server-side rendered for this example. The only alternative I can think of is kind of hacky; create a middleware that sets a header (or cookie) that is then read from that SC. I don't need these routes to be statically rendered anyway. I also tried to make a client-only provider component which would wrap around a nav link (rendered as a SC) and use tailwind's group feature, then use that group to set the nav link style based on the class name or state. This didn't work, however this resulted in the following error:
Error: usePathname only works in Client Components. Add the "use client" directive at the top of the file to use it.
Error: usePathname only works in Client Components. Add the "use client" directive at the top of the file to use it.
It seems that rendering a client component that uses useRouter or usePathname inside a server component isn't allowed (or supported), even if I don't wrap it (???) I am ok if I have to pass a prop from the layout into the main nav. That's not my main concern.
24 Replies
Ambushfall
Ambushfall2y ago
Hey, I have kind of a solution
'use client';
import Link from "next/link";
import { useSelectedLayoutSegments } from "next/navigation";


const segment = useSelectedLayoutSegments();

const isActive = href === `/${segment}`
console.log(href)
'use client';
import Link from "next/link";
import { useSelectedLayoutSegments } from "next/navigation";


const segment = useSelectedLayoutSegments();

const isActive = href === `/${segment}`
console.log(href)
the full component I use is:
'use client';
import Link from "next/link";
import { useSelectedLayoutSegments } from "next/navigation";
import {toPascalCase} from '../../helpers/toPascalCase'

function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ')
}


export function NavLinkTailwind({ href, NavItemName }: { href: string; NavItemName: string; }) {
const segment = useSelectedLayoutSegments();

const isActive = href === `/${segment}`
// console.log(href)

return (
<Link href={href} className={classNames(isActive? 'bg-gray-900 text-white dark:bg-gray-400 dark:text-black' : 'dark:text-gray-400 dark:hover:bg-gray-800 text-black hover:bg-gray-700 hover:text-white','px-3 py-2 rounded-md text-2xl font-medium')} aria-current={isActive ? 'page' : undefined}>
{toPascalCase(NavItemName)}
</Link>
);
}
'use client';
import Link from "next/link";
import { useSelectedLayoutSegments } from "next/navigation";
import {toPascalCase} from '../../helpers/toPascalCase'

function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ')
}


export function NavLinkTailwind({ href, NavItemName }: { href: string; NavItemName: string; }) {
const segment = useSelectedLayoutSegments();

const isActive = href === `/${segment}`
// console.log(href)

return (
<Link href={href} className={classNames(isActive? 'bg-gray-900 text-white dark:bg-gray-400 dark:text-black' : 'dark:text-gray-400 dark:hover:bg-gray-800 text-black hover:bg-gray-700 hover:text-white','px-3 py-2 rounded-md text-2xl font-medium')} aria-current={isActive ? 'page' : undefined}>
{toPascalCase(NavItemName)}
</Link>
);
}
you use this inside of a server component, yes the link itself still renders as a client component you just pass the values from the server component sorry if it's not what you're looking for
human
humanOP2y ago
Yeah, not what I was looking for since it's a client component. Thanks anyway. I did figure out using the provider (I had a usePathname in my nav that is an SC, that's why it failed). But really, I just wonder why we have to go through hoops to get such context.
Ambushfall
Ambushfall2y ago
Idk, for me personally, it makes all sense to use client components for links, buttons, etc. And to pass data to them from SC Hopefully you find a great workaround!
human
humanOP2y ago
For me it doesn't, because they are non-interactive in terms of JS. They don't enhance the experience as they are just anchors. For buttons, I'd rather use forms to handle data submissions. Makes logic a lot simpler and avoids a ton of footguns that comes with managing state and side effects.
alan
alan2y ago
holy crap. I can't believe this is even an issue. I was looking for this and it seems like there's a high demand for knowing the pathname in a server component but vercel dropped the ball on this
alan
alan2y ago
GitHub
[Next 13] Server Component + Layout.tsx - Can't access the URL / Pa...
Verify canary release 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...
Grey
Grey2y ago
The soloution I did was just add a x-current-url via my middleware for the headers() to have access to it works as expected
// middleware.ts

import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
// your middleware stuff here
return NextResponse.next({
request: {
headers: new Headers({ "x-url": request.url }),
},
});
}

// page.tsx

import React from "react";
import { headers } from "next/dist/client/components/headers";

export default function Page() {
const _headers = headers();
const currentUrl = _headers.get("x-url");
return (
<div>
<span>Some page at url {currentUrl ?? ""}</span>
</div>
);
}
// middleware.ts

import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
// your middleware stuff here
return NextResponse.next({
request: {
headers: new Headers({ "x-url": request.url }),
},
});
}

// page.tsx

import React from "react";
import { headers } from "next/dist/client/components/headers";

export default function Page() {
const _headers = headers();
const currentUrl = _headers.get("x-url");
return (
<div>
<span>Some page at url {currentUrl ?? ""}</span>
</div>
);
}
@humanest ^
alan
alan2y ago
is middleware.ts in /app/middleware.ts?
Grey
Grey2y ago
yes core root*
alan
alan2y ago
I'm seeing null when logging currentUrl
Grey
Grey2y ago
it should work, I used it the other day and remember it being something like that
alan
alan2y ago
hrm.. this is what I have
// layout.tsx
import { headers } from "next/headers";

export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const _headers = headers();
const currentUrl = _headers.get("x-url");
console.log("url>>", currentUrl);
return <div>{children}</div>;
}
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
// your middleware stuff here
console.log("reqqq", request);
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-url", request.url);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
// layout.tsx
import { headers } from "next/headers";

export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const _headers = headers();
const currentUrl = _headers.get("x-url");
console.log("url>>", currentUrl);
return <div>{children}</div>;
}
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
// your middleware stuff here
console.log("reqqq", request);
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-url", request.url);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
and seeing nothing logged for request and null logged for currentUrl 😦 I also tried your code verbatim using "next/dist/client/components/headers";
alan
alan2y ago
Stack Overflow
How can I get the url pathname on a server component next js 13
So basically I have a server component in app dir and I want to get the pathname. I tried to do it using window.location but it does not work. Is there any way I can do this?
Grey
Grey2y ago
the in the world are you passing the request.headers to new Headers() look my at example above clearly 🤨
alan
alan2y ago
that's from the linked solution. I tried yours too I'll try yours again and paste it
Grey
Grey2y ago
well you're clearly doing something wrong, because I just strapped a new next 13 app and it worked
alan
alan2y ago
// src/app/middleware.ts
import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
// your middleware stuff here
console.log("reqqq", request);
return NextResponse.next({
request: {
headers: new Headers({ "x-url": request.url }),
},
});
}

// src/app/dashboard/layout.tsx

import { headers } from "next/headers";

export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const _headers = headers();
const currentUrl = _headers.get("x-url");
console.log("url>>", currentUrl);
return <div>{children}</div>;
}
// src/app/middleware.ts
import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
// your middleware stuff here
console.log("reqqq", request);
return NextResponse.next({
request: {
headers: new Headers({ "x-url": request.url }),
},
});
}

// src/app/dashboard/layout.tsx

import { headers } from "next/headers";

export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const _headers = headers();
const currentUrl = _headers.get("x-url");
console.log("url>>", currentUrl);
return <div>{children}</div>;
}
Grey
Grey2y ago
Grey
Grey2y ago
I literally copy/pasted your snippet now and it works you're doing something else wrong if it's not working
alan
alan2y ago
moving middleware.ts into /src from /src/app fixed it man, I wish the docs were clearer... it says the "root of the project". like does that mean it's colocated with package.json, which would be outside of src? thanks for your help
Grey
Grey2y ago
root is root of /app or /pages, I should've clarified that subconsciously 🤔
alan
alan2y ago
meh. it's on vercel to disambiguate. the root can mean so many things up to the app/pages dir
Grey
Grey2y ago
well you can always open a PR on their docs now that they're Open-Source 🤔
alan
alan2y ago
good point
Want results from more Discord servers?
Add your server