iT邦幫忙

2023 iThome 鐵人賽

DAY 9
1
Modern Web

深入淺出,完整認識 Next.js 13 !系列 第 9

Day 09 - Persistent Layout 是什麼?要怎麼在 App Router 中實踐?

  • 分享至 

  • xImage
  •  

昨天介紹了 App Router 的第一個優點,是能讓專案的檔案結構更加自由。今天接著來分享第二個優點:可以透過特殊檔案 layout.tsx 更簡單地實現 persistent layout。

什麼是 persistent layout?簡單來說就是路由切換時,沒有變動的部分不會 re-render,讓 state 和頁面狀態 ( ex: 滾輪位置 ) 可以維持一樣。

舉例來說:

我們想做一個設定頁面 ( /settings ),sub routes 包含像是 /profile、 /account、/notifications 等不同設定。

每個 sub routes 都有一個共用的 scroll bar,這個 scroll bar 的寬度固定,超出寬度的部分則用 overflow-auto 來讓使用者左右滾動查看。點 scrollbar 上的選項時,則會轉到對應的頁面 ( ex: 點 Profile -> settings/profile )
scoll bar 點擊示意圖

假如在 Pages Router 實作,有幾種做法:

方法一:寫一個 ScrollBar 的共用元件,並在 /pages/settings 的頁面檔案導入

import Scrollbar from '@/components/Scrollbar';

export default function Profile() {
  return (
    <>
      <Scrollbar />
      Profile
    </>
  );
}

但這個方法一方面很麻煩,每個要用的頁面都要 import Scrollbar;另一方面,當我們滾到最右邊並點擊 About 時,路由會切到 /about,但 scroll bar 會回到起始位置,並沒有停留在最右邊:
沒有 persistent 的 scoll bar 點擊示意圖

原因是:page components 永遠會在 component tree 的最頂層。當 page components 改變時,React 會連同 child components 整個重新渲染。所以就算新的頁面也有 ScrollBar,Scrollbar 還是會被重做,也因此 scroll bar 會回到初始位置。

而作為 SPA 重點之一,persistent layout 即是希望當 URL 切換時,不需改動的 UI 元素可以不 re-render,保留某些狀態 ( ex: 當前 scroll bar 位置 ),增強使用者體驗。很顯然第一個的方法既不符合 DRY 原則 ( Don't Reapeat Yourself ),在 UX 上也不友善。

方法二:使用 getLayout() 創建一個含有 scroll bar 的 layout
想改善 persistent layout 的問題,我們可以在_app.tsx中,用 <Component> 中的其中一個 property getLayout() 來建一個有 scroll bar 的共用元件:

/* pages/_app.tsx */
import '@/styles/globals.css';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
import type { ReactElement, ReactNode } from 'react';

export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode;
};

type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout;
};

export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  const getLayout = Component.getLayout ?? ((page) => page);

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

要使用這個 layout 的頁面,可以將頁面內容以 props 方式傳進這個 layout 中:

/* pages/settings/profile.tsx */
import Header from '@/components/Scrollbar';
import type { ReactElement } from 'react';

export default function Page() {
  return <div>Profile</div>;
}

Page.getLayout = function getLayout(page: ReactElement) {
  return (
    <>
      <Header />
      {page}
    </>
  );
};

這個做法的確達到了 persistent layout,但一樣,每個要使用的頁面都要 import 這個 layout。
有 persistent 的 scoll bar 點擊示意圖

方法三:在 _app.tsx 中加入 scroll bar,並用 URL 判斷是否顯示
第三種方法,我們在 /pages/_app.tsx import ScrollBar,並加一個條件式讓它只有在 /settings 底下的 routes 中顯示:

import type { AppProps } from 'next/app';
import { usePathname } from 'next/navigation';

export default function App({ Component, pageProps }: AppProps) {
  const pathname = usePathname();
  const isSettingsPage = pathname.startsWith('/settings');

  return (
    <div className='max-w-xl mx-auto px-[30px]'>
      {isSettingsPage && <Scrollbar />}
      <Component {...pageProps} />
    </div>
  );
}

這個做法也可以達到 persistent layout,也不用每個要使用的檔案都 import layout,但假如今天 layout 很多,可能會讓 _app.tsx 很難維護。

要怎麼讓開發者能更輕鬆地產生 persistent layout 也成為了 Next.js 13 改版的核心重點之一,因此 App Router 版本推出了一個新的 file convention - layout.js/jsx/tsx

The developer experience of creating layouts can be improved. It should be easy to create layouts that can be nested, shared across routes, and have their state preserved on navigation.

Next.js - Layouts RFC

Layout 是一個可以在路徑底下的子路徑中,共用的 UI。它不會影響 routing 而且當使用者在子路由之間切換時,也不會 re-render。

以剛剛的例子來說,我們可以在 /app/settings中建一個 layout.tsx,此檔案中 default export 的 component 就會被當作 /settings 路由下的 layout:

/* app/settings/layout.tsx */
import ScrollBar from '@/components/ScrollBar';
import React from 'react';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <ScrollBar />
      {children}
    </>
  );
}

/settings 底下的頁面元件就會以 children props 的方式傳入 Layout 中,達到元件共用且切換路由時 scroll bar 會維持原本位置的效果!

Layout 又分為 Root Layout 與 Regular Layout:

  • Root Layout:
    /app 根目錄中的 layout.tsx,root layout 中定義的 UI 會套用到所有頁面中,可以用來取代 Pages Router 中的 _app.tsx 和 _document.tsx。
    https://ithelp.ithome.com.tw/upload/images/20230909/20161853h0gqqEMN94.png

注意事項:

  1. App Router 中一定要有至少一個 root layout ( 可以透過 route groups 來創造多個 root layout,之後會和大家分享 )。
  2. Root layout 中一定要包含 <html><body> tags,因為 Next.js 不會自動生成
  3. 你可以自訂初始 HTML 檔案的內容,像是 <head> <title> 等,但後續會跟大家分享官方建議設定 metadata 的方式。
  4. Root layout 只能是 Server Components。
/* app/layout.tsx */
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import Header from './Header';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body className={inter.className}>
        <Header />
        {children}
      </body>
    </html>
  );
}

  • Regular Layout
    /app 底下資料夾中的 layout.tsx,該 route 的子路由都會套用 regular layout 定義的 UI。

比方說,在 /dashboard 中建立一個 layout.tsx,則 /dashboard/about,與 /dashboard/settings,都會套用到此 layout。
https://ithelp.ithome.com.tw/upload/images/20230909/20161853QHzxTLllIv.png
注意事項:

  1. layout 預設會是巢狀的,意思是 regular layout 也會被以 children props 的形式傳到 root layout。以上方圖片為例,/dashboard 的子路由會同時吃到 root layout 和 dashboard layout。
    https://ithelp.ithome.com.tw/upload/images/20230909/20161853zvhPytoTzf.png
  2. Regular layout 中不能使用 <head><body> tag。

今天就先到這邊,後續介紹 routing convention 的時候會再和大家更近一步介紹 page、layout、template、loading 等等特殊檔案!明天開始會和大家介紹 v13 的另個改版重點,相信大家也耳熟能詳的 Server Components!

謝謝大家耐心的閱讀,我們明天見!


上一篇
Day 08 - App Router 是什麼?
下一篇
Day 10 - Server Components 是什麼?跟 Server Side Rendering 一樣嗎?
系列文
深入淺出,完整認識 Next.js 13 !30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言