惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Spread Privacy
Spread Privacy
K
Kaspersky official blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Forbes - Security
Forbes - Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
The Last Watchdog
The Last Watchdog
SecWiki News
SecWiki News
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Secure Thoughts
WordPress大学
WordPress大学
Microsoft Security Blog
Microsoft Security Blog
P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog
Security Latest
Security Latest
TaoSecurity Blog
TaoSecurity Blog
Cyberwarzone
Cyberwarzone
S
SegmentFault 最新的问题
Cloudbric
Cloudbric
aimingoo的专栏
aimingoo的专栏
S
Schneier on Security
N
Netflix TechBlog - Medium
MyScale Blog
MyScale Blog
T
The Blog of Author Tim Ferriss
H
Hacker News: Front Page
C
Cybersecurity and Infrastructure Security Agency CISA
小众软件
小众软件
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
AWS News Blog
AWS News Blog
AI
AI
G
GRAHAM CLULEY
IT之家
IT之家
P
Privacy & Cybersecurity Law Blog
L
Lohrmann on Cybersecurity
Last Week in AI
Last Week in AI
D
Docker
Recent Announcements
Recent Announcements
O
OpenAI News
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
S
Security @ Cisco Blogs
T
Troy Hunt's Blog
C
Check Point Blog
博客园 - 三生石上(FineUI控件)
A
About on SuperTechFans
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
N
News and Events Feed by Topic

Adam Wathan's Blog

Composing the Uncomposable with CSS Variables Tailwind CSS: From Side-Project Byproduct to Multi-Million Dollar Business Going Full-Time on Tailwind CSS 2018 Year in Review The Trick to Understanding Scoped Slots in Vue.js Renderless Components in Vue.js Don't Use Em for Media Queries CSS Utility Classes and "Separation of Concerns" The $61,392 Book Launch That Let Me Quit My Job
Persistent Layout Patterns in Next.js
2019-10-17 · via Adam Wathan's Blog

When single-page applications were really getting popular in the early Backbone/Ember/Angular days, one of the biggest selling points was that you could navigate around your site without re-rendering the entire document from scratch every time the URL changed.

This meant you could do things like preserve the scroll position in part of the UI that didn't change (like a sidebar for example) without the complexity of measuring it and trying to restore it on the next page load like you'd have to do in a traditional server-driven application.

Because this benefit was so heavily advertised, I was very surprised to find out that in many modern single-page application frameworks like Next.js and Gatsby, the default experience is re-rendering the entire UI every time you click a link — throwing away that nice feeling of a persistent UI we worked so hard to achieve a decade ago!

Next.js is such a wonderfully productive development experience and produces such incredibly fast websites that I just refused to believe it had to be this way.

So I spent a few weeks researching, asking questions, and experimenting, and came up with these four patterns for persistent layouts in Next.js applications.

The default experience

Check out this little demo app I've put together:

It's got two main sections:

  • A home screen, which is just a single page.
  • An account settings section, which contains a horizontally scrollable list of tabs that you can click to visit different subsections.

In this demo, the page components are constructed in the simplest, most naive way possible, where the pages themselves use the layout components they need as their own root element:

// /pages/account-settings/basic-information.js
import AccountSettingsLayout from '../../components/AccountSettingsLayout'

const AccountSettingsBasicInformation = () => (
  <AccountSettingsLayout>
    <div>{/* ... */}</div>
  </AccountSettingsLayout>
)

export default AccountSettingsBasicInformation

The problem with this approach is most obvious when you visit one of the account settings pages, scroll the list of tabs all the way to the right, and click the "Security" tab.

Notice how the tab bar jumps all the way back to the left?

That's because using this approach, the component at the top of the component tree is always the page component itself, even if the first node in each page component is the same layout component.

Because the page component changes, React throws out all of its children and re-renders them from scratch, meaning each page renders a brand new copy of the SiteLayout or AccountSettingsLayout, even if the previous page had already rendered that component in the same place in the DOM.

The end result is a single-page application that feels like a server-driven application UI. Yuck!

Option 1: Use a single shared layout in a custom `<App>` component

One way to add a persistent layout component to your site is to create a custom App component, and always render your site layout above the current page component in the component tree.

// /pages/_app_.js
import React from 'react'
import App from 'next/app'
import SiteLayout from './components/SiteLayout'

class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props
    return (
      <SiteLayout>
        <Component {...pageProps}></Component>
      </SiteLayout>
    )
  }
}

export default MyApp

Here's what that looks like when applied to the demo app from earlier:

You can confirm that the SiteLayout component is not re-rendered by typing something into the search field, then navigating to another page.

Because the SiteLayout component is re-used across page transitions, whatever you type in the search field is preserved — progress!

But we still have the tab bar scroll position problem to deal with.

That's because using this approach, we can only provide a single layout component and that component must be shared across all pages.

That means the wrapper component we add here has to be the lowest common denominator between all pages on the site, and our homepage doesn't use the extra UI from the AccountSettingsLayout so we still have to render that component as the first node in each account settings page:

// /pages/account-settings/basic-information.js
import AccountSettingsLayout from '../../components/AccountSettingsLayout'

const AccountSettingsBasicInformation = () => (
  <AccountSettingsLayout>
    <div>{/* ... */}</div>
  </AccountSettingsLayout>
)

export default AccountSettingsBasicInformation

So while this approach might work if you really only need one layout and it's used for every single page on your site, it falls apart very quickly for even the most basic of projects.

Option 2: Render a different layout in `<App>` based on the current URL

If your needs are only slightly more complex than what's possible with a single shared layout, you might be able to get away with rendering a different tree based on the current URL.

By inspecting router.pathname, you can figure out which "section" of your site you're currently in, and render the corresponding layout tree:

// /pages/_app_.js
import React from 'react'
import App from 'next/app'
import SiteLayout from '../components/SiteLayout'
import AccountSettingsLayout from '../components/AccountSettingsLayout'

class MyApp extends App {
  render() {
    const { Component, pageProps, router } = this.props

    if (router.pathname.startsWith('/account-settings/')) {
      return (
        <SiteLayout>
          <AccountSettingsLayout>
            <Component {...pageProps}></Component>
          </AccountSettingsLayout>
        </SiteLayout>
      )
    }

    return (
      <SiteLayout>
        <Component {...pageProps}></Component>
      </SiteLayout>
    )
  }
}

export default MyApp

As long as you make sure you always render the entire layout tree directly in _app.js (don't use SiteLayout from within AccountSettingsLayout for example), this works perfectly:

Even the tabs work this way!

The downside of this approach is that your App component is going to be constantly churning as you add new sections to your site, and quickly grow out of control if you aren't careful about looking for opportunities to extract the route matching logic.

It also couples your layouts to your URLs, and if you ever needed to deviate (maybe /account-settings/delete uses a totally different layout), you need to add more and more hyper-specific conditional logic.

So while this can work well for smaller sites, for larger sites you'll want something more declarative and flexible.

Option 3: Add a static `layout` property to your page components

One way to avoid your App component growing in complexity over time is to move the responsibility of defining the page layout to the page component instead of the App component.

You can do this by attaching a static property like layout (the name can be whatever you want) to your page components and reading that from inside your App component:

// /pages/account-settings/basic-information.js
import AccountSettingsLayout from '../../components/AccountSettingsLayout'

const AccountSettingsBasicInformation = () => <div>{/* ... */}</div>

AccountSettingsBasicInformation.layout = AccountSettingsLayout

export default AccountSettingsBasicInformation
// /pages/_app_.js
import React from 'react'
import App from 'next/app'

class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props
    const Layout = Component.layout || (children => <>{children}</>)

    return (
      <Layout>
        <Component {...pageProps}></Component>
      </Layout>
    )
  }
}

export default MyApp

Here's what that looks like in action:

This is almost perfect, but there's one issue:

The search field state is not preserved when navigating from the home page to an account settings page, or vice versa.

That's because in this example, the AccountSettingsLayout uses the SiteLayout internally, which means the actual top level layout component is switching from SiteLayout to AccountSettingsLayout, and the original SiteLayout is destroyed and replaced with a new instance created inside of AccountSettingsLayout.

If that sort of limitation isn't a problem for your site, this can be a great option. If it is, thankfully there's one more pattern we can try.

Option 4: Add a `getLayout` function to your page components

If we use a static function instead of a simple property, we can return a complex layout tree instead of a single component:

// /pages/account-settings/basic-information.js
import SiteLayout from '../../components/SiteLayout'
import AccountSettingsLayout from '../../components/AccountSettingsLayout'

const AccountSettingsBasicInformation = () => <div>{/* ... */}</div>

AccountSettingsBasicInformation.getLayout = page => (
  <SiteLayout>
    <AccountSettingsLayout>{page}</AccountSettingsLayout>
  </SiteLayout>
)

export default AccountSettingsBasicInformation

Then in our custom App component, we can invoke that function passing in the current page to get back the entire tree:

// /pages/_app.js
import React from 'react'
import App from 'next/app'

class MyApp extends App {
  render() {
    const { Component, pageProps, router } = this.props

    const getLayout = Component.getLayout || (page => page)

    return getLayout(<Component {...pageProps}></Component>)
  }
}

export default MyApp

(I've used the name getLayout in this example but it can be whatever you want — this isn't a framework feature or anything.)

This puts each page component in charge of its entire layout, and allows an arbitrary degree of UI persistence:

Bonus: Add a `getLayout` function to your layout components

Using this approach can mean some repetitive code in page components that use the same layout tree, and can force you to update many files if you ever need to change that tree.

One approach I've been experimenting with to avoid this duplication is to add a getLayout function to each layout component as well, so page components can delegate to the layout in order to fetch the complete layout tree.

// /components/SiteLayout.js
const SiteLayout = ({ children }) => <div>{/* ... */}</div>

export const getLayout = page => <SiteLayout>{page}</SiteLayout>

export default SiteLayout
// /components/AccountSettingsLayout.js
import { getLayout as getSiteLayout } from './SiteLayout'

const AccountSettingsLayout = ({ children }) => <div>{/* ... */}</div>

export const getLayout = page =>
  getSiteLayout(<AccountSettingsLayout>{page}</AccountSettingsLayout>)

export default AccountSettingsLayout

Now each page component simply imports getLayout from the layout it needs, and re-exports it as a static property on the page itself:

// /pages/account-settings/basic-information.js
import SiteLayout from '../../components/SiteLayout'
import { getLayout } from '../../components/AccountSettingsLayout'

const AccountSettingsBasicInformation = () => <div>{/* ... */}</div>

AccountSettingsBasicInformation.getLayout = getLayout

export default AccountSettingsBasicInformation

Here's what this looks like applied to the entire demo:

Eventually I'd love to see persistent layouts get more love as a first-class feature in Next.js, but for now hopefully these patterns will let you recreate that classic SPA user experience without having to give up all of the wonderful things Next has to offer.

If you have any questions or ideas, I'm @adamwathan on Twitter!