Prerequisites
- A working
next@13.x.x+
project with appDir - Basic knowledge of the next.js framework
- Basic knowledge of the next.js middleware
- Basik knowledge of what an HTTP header is
- An innocent layout file
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.
- We created a middleware that runs before every request.
- We got the nextUrl, which is basically the address bar.
- 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.