How to Persist Components Between Pages in Next.js (And why it works)

The biggest question I had about Next.js (as an SSR framework) is:

  • If the pages are rendered on the server-side, how can I persist components (like a header) between page changes?

Turns out the presumption is wrong. Next.js actually uses client-side rendering for page navigation. In other words, a Next.js app behaves just like a regular SPA, except for the first page you request from it (which is rendered on the server).

But it's not that simple either. If you put a common component (imported from somewhere in your /components folder) in two different pages (defined in /pages) in Next.js, then the component will actually be re-rendered when you navigate from one page to the other.

Let's take a real example:

  • Go to https://nextjs.org/blog (it's built with Next.js);
  • In the inspector, change the background of the header to yellow;
  • Click the "Analytics" link in the header to navigate to https://nextjs.org/analytics;
  • Notice that the yellow background disappeared. This means the header - even though consistent between the 2 pages - is re-rendered.

(Or you can create a new Next.js project and test it yourself.)

This is not what we would expect from client-side rendering. It doesn't make sense to re-render the same component if the page is rendered on the client-side!

The Custom App Component

In Next.js, the correct way to persist components between page changes is to use the custom App component.

It's quite simple. All you have to do is to create the file /pages/_app.js if it doesn't exist already, and add the components you want to persist in there.

For example, the following code will persist the <Layout /> component between page changes:

// pages/_app.js
import Layout from '../components/layout'

function MyApp({ Component, pageProps }) {
  return (
    // Add your header, footer, etc. in Layout and they will persist
    <Layout>
      // Don't change this. `Component` will be set to the current page component
      <Component {...pageProps} />
    </Layout>
  )
}

export default MyApp

But why do we have to use this App component? What's happening under the hood?

How Next.js Renders Your Page

To answer the above question, we have to understand what really happens when you navigate from one page to another in Next.js.

Let's say you are navigating from the page <Foo /> to the page <Bar /> (defined in pages/foo.js and pages/bar.js respectively) by clicking a Next.js link. Here is what will happen:

  1. The JavaScript code of the new page component <Bar /> is fetched from the server, if it's not already prefetched;
  2. Next.js will call ReactDOM.render() with 2 arguments: the first one is the new React element to render (it can be roughly thought of as the updated App component), and the second one is the DOM container element (it's always <div id="__next"></div>) that the new React element is rendered into.

In short, this process can be roughly thought of as rendering the updated App component into the <div id="__next"></div> DOM container element. React will then take care of diffing the new and old React elements and decide which part of the DOM to re-render and which part to update.

So what does the new and old React element look like? Well, the default definition of the App component looks like this:

import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  // Component will be set to the current page component
  return <Component {...pageProps} />
}

export default MyApp

Where the Component variable will be set to the current page component. This means the old React element will look like this:

<Foo {...pageProps} />

and the new React element will look like this:

<Bar {...pageProps} />

According to the React diffing algorithm, when comparing the new and old React element, if the two elements being compared are of different types, then the corresponding subtree will be entirely destroyed and re-rendered.

That's exactly what happens in this case. <Foo /> and <Bar /> are two different components and are considered as of different types, so the part of the DOM that corresponds to <Foo /> will be destroyed and re-rendered as <Bar />.

That's why the entire page component will be re-rendered when you navigate to a new page, even if they include common components like the header.

And that's why the custom App component approach works. If you're using the suggested custom App component above, then the old React element will look like this:

<Layout>
  <Foo {...pageProps} />
</Layout>

and the new React element will look like this:

<Layout>
  <Bar {...pageProps} />
</Layout>

In this case, the page component <Foo /> is still going to be destroyed and re-rendered as <Bar />, but <Layout /> will persist.

22