faslin_kosta.com
hero image

How to get searchParams in Next.js layout.tsx, SSR


Prerequisites

Layout file

As we all know, layouts receive only params and children as props. That means that searchParams is off the table… well, it’s actually not.

You see, when in layout files, we still have access to the cookies() and headers() functions. While cookies() is not of use in this situation, headers() is pretty useful. We can use those headers to store our url and access it from the layout.

Let’s create a layout file:

export default function Layout(props: LayoutProps) {
  const headersStore = headers()
  const headersAsArray = Array.from(headersStore.entries())

  return <div>{props.children}</div>
}

The “headers()” function

To access the headers in an SSR file (not just layout), we must import the headers function from next/headers

After that, we must call it to get the headers store. Or basically an object with methods for accessing the headers.

We can log the headersStore constant to see what’s hiding inside. Turns out, just some key/value pairs. Great, but how does this help us?

import { headers } from 'next/headers'

export default function Layout(props: LayoutProps) {
  const headersStore = headers()
  const headersAsArray = Array.from(headersStore.entries())

  return <div>{props.children}</div>
}

The middleware

Let’s create a middleware. If you already have one, ignore this step.

Go to the root of your project or in the src folder, if you are using one. The file must be adjacent to the app folder. Create a file called middleware.js/ts and export a function from it:

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

export async function middleware(request: NextRequest) {
  const normal_response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  })
  return normal_response
}

We are creating a new response and we are calling the next method, which tells the middleware to continue routing. And then we are copying the request’s headers to the request… Which at the moment does nothing, but it will make sense in the next step.

Edit your middleware

Now that we have a middleware, we must set up the trick. Get the nextUrl object, that sits inside the NextRequest . This object extends the default URL cAPI with additional methods such as pathname and searchParams wink.

We need to call the toString() method that basically gets a string, equal to the address bar of your browser. With the protocol, search parameters, everything.

const url = request.nextUrl.toString() // https://whatever-domain.com?foo=bar#baz

Now that we have this url, we can append it to our request’s headers. Why request and not response? Because we want to send that header to the Server Side Rendered layout. We should use a name that starts with x-, because that’s what the convetion tells us. Custom headers should start with x-

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

export async function middleware(request: NextRequest) {
  const request_headers = new Headers(request.headers)

  request_headers.append('x-middleware-next-url', request.nextUrl.toString())

  const normal_response = NextResponse.next({
    request: {
      headers: request_headers,
    },
  })

  return normal_response
}

The response requires the headers to be an instance of Headers, that’s why we use the default API and not just an object.

Let’s retrospect what we did.

  1. We created a middleware that runs before every request.
  2. We got the nextUrl, which is basically the address bar.
  3. We passed it to the server.

Now, we must capture it!

The trick

Let’s go back to our layout and get our header.

import { headers } from 'next/headers'

export default function Layout(props: LayoutProps) {
  const headersStore = headers()
  const url = new URL(headersStore.get('x-middleware-next-url'))

  return <div>{props.children}</div>
}

Here we can actually reconstruct the browser’s url from the string using the default URL API. But we can do it better. Let’s use what next.js uses in its middleware. The NextURL

import { headers } from 'next/headers'
import { NextURL } from 'next/dist/server/web/next-url'

export default function Layout(props: LayoutProps) {
  const headersStore = headers()
  const url = new NextURL(headersStore.get('x-middleware-next-url'))

  return <div>{props.children}</div>
}

This class NextURL extends the default URL API and provides us with methods of getting the searchParams and the pathname wink

import { headers } from 'next/headers'
import { NextURL } from 'next/dist/server/web/next-url'

export default function Layout(props: LayoutProps) {
  const headersStore = headers()
  const url = new NextURL(headersStore.get('x-middleware-next-url'))

  const page = url.searchParams.get('page')
  const pathname = url.pathname

  return <div>{props.children}</div>
}

And there we have it - searchParams and pathname in layout.jsx/tsx file.

Without middleware

We can do this trick even without the middleware. If we inspect the headers, we can sometimes see that there is a referer header, that is the same as request.nextUrl.toString(). But this referer only appears when we navigate to the page, not when we open it directly, which can break your logic, hence - the middleware way.

import { headers } from 'next/headers'
import { NextURL } from 'next/dist/server/web/next-url'

export default function Layout(props: LayoutProps) {
  const headersStore = headers()
  const url = new NextURL(headersStore.get('referer')) // there is no referer when you open the page by pasting the link or refreshing.

  const page = url.searchParams.get('page')
  const pathname = url.pathname

  return <div>{props.children}</div>
}

Caveats

When using headers() or cookies(), next opts out of static rendering, which means the page will no longer be statically rendered, even if you do not use search params in your url. But given the fact that you need access to the search params, this page wasn’t going to be static in the first place.

Make sure to not use this trick in a large tree of child pages. Layouts are meant to be static between route changes and render only once to make up for better optimization. If a layout needs something dynamic inside of it, this can be achieved by using a client component or writing the logic in a reusable component and placing it in the page file.

author

About the author

Vasil Kostadinov

Software developer, specializing in React, Next.js and Astro for the frontend, as well as in Supabase and Strapi for the backend. Creates content in all shapes & sizes in his free time.