🤝

Next.js12.xのLayouts機能

2022/02/11に公開

Next.jsのLayoutについてTypeScriptの記事を探してもあまり見つからなかったので
https://nextjs.org/docs/basic-features/layouts

Layoutsとは🤔

ページ間で共通のコンポーネントを定義する機能
HeaderやFooterなどはほぼ全ページ同じレイアウトを使うけど、ページコンポーネントに直接書くとコンポーネントの状態(入力値など)が保持できないのでページコンポーネントの外に書くことでレイアウトの状態も保持できる🤝

検証に使ったリポジトリ

https://github.com/hisho/next-layouts-demo

Next.jsのLayoutsを使うと何が嬉しいのか

一部共通ではないページが有る場合もページ単位でLayoutを変えられるので対応可能😎
下記の共通コンポーネントをNext.jsのLayoutsに置き換えてみる

共通のcomponents

components/Header/Header.tsx
import { memo } from 'react'

let render = 0

export const Header = memo(() => {
  console.log(`render${render}`)
  render++
  return <header>header</header>
})
components/Footer/Footer.tsx
import { memo } from 'react'

export const Footer = memo(() => <footer>footer</footer>)
layout/Layout.tsx
import { ReactElement } from 'react'
import { Footer, Header } from '@src/component/ui'

type LayoutProps = Required<{
  readonly children: ReactElement
}>

export const Layout = ({ children }: LayoutProps) => (
  <>
    <Header />
    {children}
    <Footer />
  </>
)

_app.tsxに直接書くパターン

pages/_app.tsx
import type { AppProps } from 'next/app'
import { Layout } from '@src/layouts'

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}

export default MyApp

メリット

  • _app.tsxに書くだけで済む

デメリット

  • 共通のレイアウト以外が出てきた時に成立しない

絶対共通のLayoutになるとは限らないしデメリットの方が大きいので、Next.jsのLayoutsを使いページ単位で共通するLayoutを定義した方が良い☺️

Next.jsのLayoutsを使うパターン

nextの型を拡張する

型は公式から引用
https://nextjs.org/docs/basic-features/layouts#with-typescript

types/index.d.ts
import type { NextPage, NextPageWithLayout } from 'next'
import type { AppProps } from 'next/app'
import type { ReactElement } from 'react'

declare module 'next' {
  type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
    getLayout?: (page: ReactElement) => ReactElement
  }
}

declare module 'next/app' {
  type AppPropsWithLayout<P = {}> = AppProps<P> & {
    Component: NextPageWithLayout<P>
  }
}

_app.tsxにgetLayoutを追記する

Component.getLayoutがない場合はそのままpageを返す

pages/_app.tsx
-import type { AppProps } from 'next/app'
+import type { AppPropsWithLayout } from 'next/app'
-import { Layout } from '@src/layouts'

-function MyApp({ Component, pageProps }: AppProps) {
+function MyApp({ Component, pageProps }: AppPropsWithLayout) {
+  const getLayout = Component.getLayout ?? ((page) => page)
-  return (
-    <Layout>
-      <Component {...pageProps} />
-    </Layout>
-  )
+  return getLayout(<Component {...pageProps} />)
}

export default MyApp

共通レイアウトを各ページで呼び出す

pages/index.tsxにgetLayoutを追記する

pages/index.tsx
-import type { NextPage } from 'next'
+import type { NextPageWithLayout } from 'next'
import { HomePage } from '@src/component/pages/Home'
+import { Layout } from '@src/layouts'

-const Home: NextPage = () => <HomePage />
+const Home: NextPageWithLayout = () => <HomePage />

+Home.getLayout = (page) => <Layout>{page}</Layout>

export default Home

pages/about/index.tsxにgetLayoutを追記する

pages/about/index.tsx
-import type { NextPage } from 'next'
+import type { NextPageWithLayout } from 'next'
import { AboutPage } from '@src/component/pages/About'
+import { Layout } from '@src/layouts'

-const About: NextPage = () => <AboutPage />
+const About: NextPageWithLayout = () => <AboutPage />

+About.getLayout = (page) => <Layout>{page}</Layout>

export default About

別のLayoutを適応したい時

pages/work/index.tsx
import type { NextPageWithLayout } from 'next'
import { WorkPage } from '@src/component/pages/Work'

const Work: NextPageWithLayout = () => <WorkPage />

Work.getLayout = (page) => (
  <>
    <header>work header</header>
    {page}
    <footer>work footer</footer>
  </>
)

export default Work

おまけ

AppPropsWithLayoutを拡張して色々できそうなので、面白いことができたらまた記事を書くかも😏

Discussion