Next.js でのレイアウトの作り方と切り替え方

imatomix
2020年6月11日 20:37

公式が更新されました。

公式のレイアウトページ が更新され、新たにページコンポーネントにgetLayoutを付与するやり方の情報が追加されました。本ページのやり方でも問題はないでしょうが、こういうのは何事も公式に沿った方がよろしいかと思います。

概要

ヘッダやナビゲーションなどの、どのページでも使う共通のレイアウトコンポーネントを Next.js で作ってみました。

ちなみにReact.js だと

App.tsxの中で react-router-dom を使用して、共通レイアウトとルーティングを作っていく。
App.tsx
import React from 'react' import { BrowserRouter as Router, Switch, Route } from 'react-router-dom' import Header from 'components/Header' import Nav from 'components/Footer' import Footer from 'components/Footer' import Home from 'pages/Home' import PageA from 'pages/PageA' import PageB from 'pages/TermsB' import NotFound from 'pages/NotFound' export default function App() { return ( <Router> <LayoutHeader /> <main role="main" id="contents"> <Switch> <Route exact path="/"> <Home /> </Route> <Route exact path="/page-a"> <PageA /> </Route> <Route path="/page-b"> <PageB /> </Route> <Route path="*"> <NotFound /> </Route> </Switch> </main> <LayoutFooter /> </Router> ) }

本題、Next.js だと

まずは公式をみてみる

Next.jsでは pages ディレクトリ以下のコンポーネントからルーティングが生成される。レイアウトに関しては、公式のチュートリアルを見ると、こんな感じのLayoutコンポーネントを作って
Layout.tsx
function Layout({ children }) { return <div>{children}</div> } export default Layout
各ページコンポーネント内で、こんな感じで使う的なことが書いてある。<Layout></Layout> で挟んだ部分が上記 childrenの部分に入るから、ページがレイアウトでラッピングされる。
pages/index.tsx
import Link from 'next/link' import Layout from '../../components/layout' export default function FirstPost() { return ( <Layout> <h1>First Post</h1> <h2> <Link href="/"> <a>Back to home</a> </Link> </h2> </Layout> ) }

。。。あれ?でも、

これだとページ遷移する度にレイアウトが持つ状態も破棄して再レンダリングしちゃうのでは?
と思って、試しにヘッダにつけたプルダウンメニューを開いた状態でページ遷移してみた。そして、状態は綺麗に破棄された。

ナビゲーションメニューにアニメーションなどを入れている場合、アニメーションが遷移をまたげないのは困るので、ヘッダなどの共通コンポーネントは状態を保持しつつ、必要な部分だけ更新して欲しい。

もう少し調べたところ、ちゃんと Custom App があった。React.js でいうところの上記 App.tsx の部分、Next.js がページをレンダリングする前にやる処理を、 pages/_app.tsx でカスタムできる。

カスタム App

以下のように pages/_app.tsx にLayoutコンポーネントを配置してあげれば、ページ遷移でも状態を維持できた。
pages._app.tsx
import React from 'react' import LayoutMain from '../layouts/LayoutMain' function App({ Component, pageProps }) { return ( <LayoutMain> <Component {...pageProps} /> </LayoutMain> ) } export default App

ページ毎にレイアウトを切り替えたい

例えば、ログイン画面とログイン後のページではレイアウトが異なる、とか管理画面は違うレイアウトにしたい、といったことはよくある。そういったレイアウトの切り替えを行いたい。しかし、上記のままだと全てのページに同じレイアウトが使用されてしまうので、切り替える処理を追加したい。

ルートのパス名をみて、、、というのは、かっこ悪いのでやりたくない。できれば、Nuxt.js のようにページコンポーネント内にレイアウトを指定するような情報を持たせたい。

pages/_app.tsx の中をみると、
function App({ Component, pageProps }) { return ( <LayoutMain> <Component {...pageProps} /> </LayoutMain> ) }
気になるものがあった。
pageProps
名前からして、ページが持つプロパティ。これにレイアウト情報を持たせれば良い! つまりこんな感じで切り替えれるようにしたい。
function App({ Component, pageProps }) { switch (pageProps.layout) { case 'main': { return ( <LayoutMain> <Component {...pageProps} /> </LayoutMain> ) } default: { return ( <Component {...pageProps} /> ) } } }

ページコンポーネントにレイアウト指定情報を持たせる

各ページコンポーネントにgetServersidePropsを追記し、レイアウト情報を追加する。 getServersidePropsはページがレンダリングされる前にサーバーサイドで実行され、ページに必要なデータをprops ( = pageProps )として渡してくれる。 サーバーサイドレンダリングではなく、プリレンダリングする場合は getStaticProps を使用する。
export const getServerSideProps = async (context) => ({ props: { layout: true // 複数のレイアウトを切り替えたいときは 'MainLayout' などの文字列を用いる } })
これで一応、Next.js で共通レイアウトを作って、ページに応じて切り替えることができるようになった。