DEV Community

Cover image for Layout Persistence in Next.js
Ozan Bolel
Ozan Bolel

Posted on • Updated on

Layout Persistence in Next.js

Although the word layout is generally related to CSS page layout, the reason why I’m posting this is a bit different. Fixing the position of navbar to the top doesn't mean that the navbar wouldn't be unmounted when the user is directed to another page. For SPAs (Single Page Application), it's the expected behaviour that the navbar remains mounted throughout the routing. While it's easy to achieve this on CRA (Create React App), it can be difficult to maintain layout persistence in Gatsby or Next.js, since routing is different due to code-splitting. In this post, we will ensure the persistence of the layout component during page transitions without using any library in Next.js.

First of all, if it isn't already added, we need to add an “_app.js” file under ”/ pages”. In this way, we will be able to interfere with the initializing process of the application, and will be able to contain the page component in the layout component we will be creating soon. In its simplest form, an “_app.js” file looks like this:

export default function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}
Enter fullscreen mode Exit fullscreen mode

This is where we'll be adding layout support for our app. Let’s change the “_app.js” file we created under “/ pages” as follows:

import React from "react";

export default function MyApp({ Component, pageProps }) {
  const Layout = Component.Layout ? Component.Layout : React.Fragment;

  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}
Enter fullscreen mode Exit fullscreen mode

Component here is the component that returns for the URL path you are on. For example; if you are on the homepage, the default export from “/pages/index.js” will return here. Therefore, we'll be adding layout components we created directly to the page components. We’ll get back to that later. Let’s create our layout. We can create it under “/layouts/MyLayout.js” to keep the project tidy:

import React, { useState } from "react";

export default function MyLayout({ children }) {
  const [counter, setCounter] = useState(0);

  return (
    <>
      <p>      
        <button onClick={() => setCounter(counter + 1)}>
          Clicked {counter} Times
        </button>
      </p>

      {children}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

If we were able to achieve layout persistance with the voodoo we cast in “pages / _app.js”, the number shown by the button with the counter on it shouldn't reset during routing. So, let’s create two sample pages under the “pages” folder to test this. First, “/pages/profile.js”:

import Link from "next/link";

export default function Profile() {
  return (
    <div>
      <p>This is the Profile page.</p>
      <p>
        <Link href="/account">
          <a>Go: Account</a>
        </Link>
      </p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

So, how do we add the layout component we just created to this page component? As I mentioned before; directly…

import Link from "next/link";
import MyLayout from "../layouts/MyLayout";

export default function Profile() {
  return (
    <div>
      <p>This is the Profile page.</p>
      <p>
        <Link href="/account">
          <a>Go: Account</a>
        </Link>
      </p>
    </div>
  );
}

Profile.Layout = MyLayout;
Enter fullscreen mode Exit fullscreen mode

Well, this is not even slightly complicated. Let’s continue by adding our second sample page under “/pages/account.js”:

import MyLayout from "../layouts/MyLayout";

export default function Account() {
  return (
    <div>
      <p>This is the Account page.</p>
      <p>
        <Link href="/profile">
          <a>Go: Profile</a>
        </Link>
      </p>
    </div>
  );
}

Account.Layout = MyLayout;
Enter fullscreen mode Exit fullscreen mode

All preparations have been completed. Now I'll open the Profile page in the browser for you. And I'll click the button with the counter on it for 4 times. Then I'll go to the Account page, and the result will be:

Alt Text

The counter didn't reset during routing because these two pages share the same layout component. If I go to a different page, MyLayout will be unmounted and the counter will reset.

In this way, you can keep navigation elements like Tab Bars etc. persistent across pages. Or, you can go to the kitchen and bake a strawberry cake, I don’t know, I’m ending this post.


I hope this was useful, you can also follow me on Twitter for future content:

twitter.com/oznbll

Discussion (16)

Collapse
leowasabee profile image
Léo Stewart

Hi,
I wonder why do you bother to import MyLayout in every pages when just import MyLayout in the _app work the same, without this MyPage.Layout = MyLayout; ?
I hav checkd and id works as expected and in a simplier way.
Leo

Collapse
ozanbolel profile image
Ozan Bolel Author • Edited on

To have multiple layouts within the app. We wouldn't want all pages to share the same layout.

Collapse
leowasabee profile image
Léo Stewart • Edited on

Oh ! That make sense. Maybe it could be easier with a custom hook then ?
Like :

import { useState } from "react";

const FirstLayout = ({ children }) => {
  const [counter, setCounter] = useState(0);

  return (
    <>
      <p> First Layout     
        <button onClick={() => setCounter(counter + 1)}>
          Clicked {counter} Times
        </button>
      </p>

      {children}
    </>
  )
}

export default FirstLayout

export function withFirstLayout (Component){
  Component.Layout = FirstLayout
  return Component
}
Enter fullscreen mode Exit fullscreen mode

and the page :

import { withFirstLayout } from 'layout/FirstLayout'

const Home = () => {
  return  <div>  index   </div>
}
export default withFirstLayout(Home)
Enter fullscreen mode Exit fullscreen mode

But when we change the layout we still have a reset of the old one

Collapse
thomas159 profile image
thomas159

how can I update the title in the head using this layout?

Collapse
ozanbolel profile image
Ozan Bolel Author • Edited on

Don't. Use title inside Head from next/head instead.

Collapse
ramirezsandin profile image
Jorge • Edited on

Hi, do you mean to use the Head tag and title tag inside every page?

Would it be possible to have the Head tag in the layout component and pass the title variable somehow when you are doing Page.Layout = MyLayout?

Thread Thread
ozanbolel profile image
Ozan Bolel Author • Edited on

Yes I meant that.

It is possible but it adds an unnecessary complexity imho.

Thread Thread
joshuatuscan profile image
Joshua Tuscan

What if you want to pass a prop to the MyLayout so that you can let your nav component know what page you are on? How do you pass props to MyLayout?

Collapse
iigorcunha profile image
Igor Cunha

Any idea, how to implement Layout with typescript?

Collapse
ozanbolel profile image
Ozan Bolel Author
import * as React from "react";
import { NextPage } from "next";

type Page = NextPage & { Layout?: React.FC };
Enter fullscreen mode Exit fullscreen mode
Collapse
iigorcunha profile image
Igor Cunha

Thanks, I was blowing my mind trying to figure that.

Thread Thread
pikeas profile image
Aris Pikeas

Could you share a bit more context on how you got this working with Typescript?Component.layout is throwing "unsafe assignment of an any value".

Thread Thread
ozanbolel profile image
Ozan Bolel Author
import * as React from "react";
import { NextPage } from "next";

type Page = NextPage & { Layout?: React.FC };

const MyPageComponent: Page = () => {
  return null;
}

export default MyPageComponent;
Thread Thread
pikeas profile image
Aris Pikeas
type Page = NextPage & {
    layout?: (props: { children: ReactNode }) => JSX.Element
}
Enter fullscreen mode Exit fullscreen mode

This seems to have done the trick for me when not using fat-arrow functions.

Collapse
macgyver98 profile image
MacGyver98

Hi! thanks for your effort and share us.

What about if I need to decide between two different layouts? Where should I put the logic and how should I export Layout from my page?

Collapse
nerdherd profile image
ali • Edited on

You will assign the layout to the page in the page file.

Upon navigating to a page, _app.js will render the layout from the page component via Component.Layout as per the authors code.

// pages/home.js
import HomeLayout from '../layouts/home-layout.jsx

const HomePage = () => (
   <div>HomePage</div>
)

// assigning HomeLayout to the page property `Layout`
HomePage.Layout = HomeLayout

export default HomePage
Enter fullscreen mode Exit fullscreen mode
// pages/account.js
import AccountLayout from '../layouts/account-layout.jsx
const AccountPage = () => (
   <div>Account Page</div> 
)

AccountPage.Layout = AccountLayout

export default AccountPage
Enter fullscreen mode Exit fullscreen mode