22
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!
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?
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:
- The JavaScript code of the new page component
<Bar />
is fetched from the server, if it's not already prefetched; - 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 updatedApp
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