昨天介紹了 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 )
假如在 Pages Router 實作,有幾種做法:
方法一:寫一個 ScrollBar 的共用元件,並在 /pages/settings
的頁面檔案導入
import Scrollbar from '@/components/Scrollbar';
export default function Profile() {
return (
<>
<Scrollbar />
Profile
</>
);
}
但這個方法一方面很麻煩,每個要用的頁面都要 import Scrollbar;另一方面,當我們滾到最右邊並點擊 About 時,路由會切到 /about
,但 scroll 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。
方法三:在 _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.
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:
/app
根目錄中的 layout.tsx
,root layout 中定義的 UI 會套用到所有頁面中,可以用來取代 Pages Router 中的 _app.tsx 和 _document.tsx。注意事項:
<html>
和 <body>
tags,因為 Next.js 不會自動生成<head>
<title>
等,但後續會跟大家分享官方建議設定 metadata 的方式。/* 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>
);
}
/app
底下資料夾中的 layout.tsx
,該 route 的子路由都會套用 regular layout 定義的 UI。比方說,在 /dashboard
中建立一個 layout.tsx
,則 /dashboard/about,與 /dashboard/settings,都會套用到此 layout。
注意事項:
<head>
和 <body>
tag。今天就先到這邊,後續介紹 routing convention 的時候會再和大家更近一步介紹 page、layout、template、loading 等等特殊檔案!明天開始會和大家介紹 v13 的另個改版重點,相信大家也耳熟能詳的 Server Components!
謝謝大家耐心的閱讀,我們明天見!