💻

Next.jsのgetLayoutパターンを実際にプロダクトで使用してみてのtips

2022/07/26に公開

はじめに

この記事ではNext.jsの公式ドキュメントに記載されているLayoutsパターンであるgetLayoutパターン(Per-Page Layouts)を実際にプロダクトで使用してみてのtipsを書いていきたいと思います。
https://nextjs.org/docs/basic-features/layouts#per-page-layouts

ちなみに2022年7月時点でNext.jsは改善されたLayouts機能のRFCを公開しており、Layoutsパターンはその機能がリリースされるまでの繋ぎという立ち位置になっていると思います。
https://nextjs.org/blog/layouts-rfc

2022年7月時点のNext.jsにおけるLayouts

まずNext.jsにおけるLayoutsとは「ページ間で頻繁に再利用されるコンポーネント(ヘッダーやフッターなど)をどこに、どのように書くか」というものです。

このうち最もシンプルなのがpages/_app.tsxに実装するパターン(Single Shared Layout with Custom App)です。

pages/_app.tsx
import { AppProps } from "next/app"
import Layout from '../components/layout'

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

https://nextjs.org/docs/basic-features/layouts#single-shared-layout-with-custom-app

上記の実装ではページごとに任意のLayoutコンポーネントを使用することができません。
例えば、普通のページではLayoutコンポーネントを、マイページではMypageLayoutコンポーネントを使用したいといった場合です。

このような場合に使用されるのがgetLayoutパターンです。

components/layout/index.tsx
...

export const getLayout = (page: React.ReactElement) => {
  return <Layout>{page}</Layout>
}
pages/index.tsx
import { getLayout } from '../components/layout'

export default function TopPage() {
  return <h1>Top Page</h1>
}

TopPage.getLayout = getLayout
pages/_app.tsx
import { NextPage } from 'next'
import { AppProps } from 'next/app'
import React from 'react'

type NextPageWithLayout = NextPage & {
  getLayout?: (page: React.ReactElement) => React.ReactNode
}

type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout
}

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

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

上記をベースに実際にプロダクトで使用してみてのtipsを書いていきます。

tips

LayoutにPropsを渡したい

getLayoutは関数化してpages配下でimportして使用していましたが、LayoutによってはPropsを渡したい場合がありました。

そこでgetLayoutを返す高階関数を定義し、それをpages配下でimportして使用するようにしました。

components/layout/index.tsx
...

export const createGetLayout = (
  layoutProps?: LayoutProps
): ((page: React.ReactElement) => React.ReactNode) => {
  return function getLayout(page: React.ReactElement) {
    return <Layout {...layoutProps}>{page}</Layout>
  }
}
pages/index.tsx
import { createGetLayout } from '../components/layout'

export default function TopPage() {
  return <h1>Top Page</h1>
}

TopPage.getLayout = createGetLayout()

Layoutで状態を持っている場合にページ遷移で状態を初期化したい

getLayoutパターンではLayoutで状態を持っている場合にページ遷移で状態を保持できることはメリットの1つです。

しかし、場合によってはページ遷移で状態を初期化したい場合もあります。
例えば、ヘッダーのハンバーガーメニューの開閉状態について、ハンバーガーメニューを開きリンクをクリックしてページ遷移した場合はハンバーガーメニューを閉じたいといった場合です。
こちらは以下のような実装で対処しました。

components/layout/Header.tsx
...

const router = useRouter()

React.useEffect(() => {
  router.events.on('routeChangeStart', onClose) // ページ遷移を開始したらハンバーガーメニューを閉じる関数(onClose)を実行

  return () => {
    router.events.off('routeChangeStart', onClose)
  }
}, [router.events, onClose])

(追記)

Twitterにて初期化したいコンポーネントのkeyにユニークな値(URLなど)を渡すことで強制的に初期化する方法を教えていただきました。
実際にkeynext/routerasPathを渡してみたところ、上記の実装と同じ挙動を実現できました。
以下のツイートにもあるように、routeChangeStart時に実行する関数を考慮する必要がなくシンプルなので、こちらの実装の方がよさそうです。
https://twitter.com/aiji42_dev/status/1551831205326503936?s=20&t=0ZOMLYs-mOdr8huYdRk1nw

おわりに

以上です。
Layout RFCの機能が実際にリリースされるとお役御免にはなりそうですが、現状はgetLayoutパターンで実装するのがよさそうです。

それではよいNext.jsライフを!

Discussion