Getting the Current URL in Next.js Server Components

Getting the Current URL in Next.js Server Components

You are writing a server component using the new(ish) App Router in Next.js, when you realize you need to get the current URL’s path. This could be for a sidebar, navbar, or even just breadcrumbs at the top of your page.

Should be pretty easy, a quick Google returns usePathname, so you take your server component and start to use it:

import { usePathname } from 'next/navigation'
 
export default function Sidebar() {
  const pathname = usePathname()
  return <div>
    <SidebarOption path="/" name="Home" activePath={pathname} />
    <SidebarOption path="/billing" name="Billing" activePath={pathname} />
    <SidebarOption path="/admin" name="Admin" activePath={pathname} />
  </div>
}

Everything seems great until you run it and get this error:

You're importing a component that needs usePathname. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.

usePathname in Server Components

The error you are getting is because usePathname can’t be used in a Server Component. The simplest fix here is to convert them to client components using the "use client" directive. In our above example, this would work just fine:

"use client"

import { usePathname } from 'next/navigation'
 
export default function Sidebar() {
  const pathname = usePathname()
  return <div>
    <SidebarOption path="/" name="Home" activePath={pathname} />
    <SidebarOption path="/billing" name="Billing" activePath={pathname} />
    <SidebarOption path="/admin" name="Admin" activePath={pathname} />
  </div>
}

But what if we were actually taking advantage of server components? For example, let’s say we were doing some authorization checks:

import { usePathname } from 'next/navigation'
 
export default async function Sidebar() {
  // Do a server-side check to see if the user is allowed to view the billing page
  const canViewBilling = await checkIfUserCanViewBilling()
  
  const pathname = usePathname()
  return <div>
    <SidebarOption path="/" name="Home" activePath={pathname} />
    {canViewBilling && 
      <SidebarOption path="/billing" name="Billing" activePath={pathname} />}
    <SidebarOption path="/admin" name="Admin" activePath={pathname} />
  </div>
}

Now, the simplest option is to turn our SidebarOption component into a client component and keep Sidebar as a server component. This is pretty typical - you often need to use client components somewhere in your application, and you want to use them at the lowest level possible.

But I just want the path in a server component…

There are, however, absolutely times where the statement “why not just use a client component for this” isn’t helpful. Server Components are a valuable abstraction to take advantage of, as long as you are aware of their limitations (for example, the fact that you can only read cookies from them, but you can’t write cookies).

The common workaround for all your “Why can’t a Server Component do this?” is to use Next.js middleware.

We can create a file called middleware.ts (or src/middleware.ts if you are using the src directory) and do the following:

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

export function middleware(request: NextRequest) {
  // Log the current request pathname
  console.log("Current path:", request.nextUrl.pathname);
  return NextResponse.next();
}

export const config = {
  matcher: [
    // match all routes except static files and APIs
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
  ],
};

When we go to the / route, we’ll see:

Current path: /

How do we pass information from Next.js Middleware to Next.js Server Components

We are able to get the path… but it’s in the wrong place. We need some way to pass information from our middleware to our server component, and it turns out there’s only one option: Headers.

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

export function middleware(request: NextRequest) {
  // Add a new header x-current-path which passes the path to downstream components
  const headers = new Headers(request.headers);
  headers.set("x-current-path", request.nextUrl.pathname);
  return NextResponse.next({ headers });
}

export const config = {
  matcher: [
    // match all routes except static files and APIs
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
  ],
};

Finally, we update our server component to look for the x-current-path header:

import { headers } from "next/headers";
 
export default async function Sidebar() {
  const headerList = headers();
  const pathname = headerList.get("x-current-path");

  // ...etc
}

Bonus: If you are using @propelauth/nextjs, we added this as a utility

A common workflow when it comes to authentication is:

  • Require that the user is logged in
  • If they aren’t, redirect them to the login page
  • After they login, redirect them back to where they started

And unfortunately, the where they started part of that workflow was hard to do in server components, for all the reasons above. We just added support for this exact workflow:

import { getUserOrRedirect } from "@propelauth/nextjs/server/app-router";

export default function Home() {
  // Redirect back to this URL 
  const user = getUserOrRedirect({ returnToCurrentPath: true });
  return <div>Hello, {user.email}</div>;
}

which will automatically do all the above work for you.

And, as long as we were doing this, we decided to expose another utility getCurrentUrl, which will get the full URL on the current page, including if you are in a server component.

Summary

If you are looking to get the current URL/Path in a server component, you have two main options:

  • Convert the server component to a client component and use usePathname
  • Add middleware that passes the URL/Path as a header, and read that header in your server component

More generally, whenever you want to do something in a server component and are struggling, you also have two options:

  • Use middleware to solve your problem, and pass the result down to your server component
  • Switch to the pages router… but we’ll save that for another day