みかづきブログ・カスタム

基本的にはちょちょいのほいです。

Layoutを使ってページを跨いでもunmountされないコンポーネントをつくる 📝

ページA、ページBに共通するコンポーネント(往復する■)を読み込んだ際、普通に実装するとページを遷移するたびに■の位置は初期値に戻るので、タイミングによってはワープしたように見えます。

これをなんとかできないものかと調べてみたところ、Layoutsを使えば実現できることがわかりました。

nextjs.org

ざっくりいえば、

  • app.tsx(app.jsx)に設置したコンポーネントはページを跨いでもunmountされない
  • pageにgetLayoutという関数を生やしてappで呼べばページによって異なるコンポーネントをapp.tsx(app.jsx)に渡せる

ということでした。

実践

pages/a.tsx

import Link from 'next/link';
import Canvas from '../components/Canvas';

export default function A() {
  return (
    <>
      <Canvas />
      <ul>
        <li>
          <Link href="/a">
            <a>A</a>
          </Link>
        </li>
        <li>
          <Link href="/b">
            <a>B</a>
          </Link>
        </li>
      </ul>
      <p>A</p>
    </>
  );
}

pages/b.tsx

import Link from 'next/link';
import Canvas from '../components/Canvas';

export default function B() {
  return (
    <>
      <Canvas />
      <ul>
        <li>
          <Link href="/a">
            <a>A</a>
          </Link>
        </li>
        <li>
          <Link href="/b">
            <a>B</a>
          </Link>
        </li>
      </ul>
      <p>B</p>
    </>
  );
}

まずは普通に実装します。

当然、ページ遷移ともに、Canvasは初期化されます。

では、共通パーツをapp.tsxに移動してみましょう。

pages/app.tsx

import { AppProps } from 'next/app';
import Link from 'next/link';
import Canvas from '../components/Canvas';

function MyApp({ Component, pageProps }: AppProps) {
  return(
    <>
      <Canvas />
      <ul>
        <li>
          <Link href="/a">
            <a>A</a>
          </Link>
        </li>
        <li>
          <Link href="/b">
            <a>B</a>
          </Link>
        </li>
      </ul>
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

pages/a.tsx

export default function A() {
  return (
    <p>A</p>
  );
}

pages/b.tsx

export default function B() {
  return (
    <p>B</p>
  );
}

ページを遷移してもCanvasが初期化されなくなりました。
見栄えを良くするために、共通部をコンポーネント化します。

components/Layout.tsx

import Link from 'next/link';
import { ReactNode } from 'react';
import Canvas from './Canvas';

export default function Layout({ children }: {
  children: ReactNode;
}) {

  return (
    <>
      <Canvas />
      <ul>
        <li>
          <Link href="/a">
            <a>A</a>
          </Link>
        </li>
        <li>
          <Link href="/b">
            <a>B</a>
          </Link>
        </li>
      </ul>
      { children }
    </>
  );
}

pages/app.tsx

import { AppProps } from 'next/app';
import Layout from '../components/Layout';


function MyApp({ Component, pageProps }: AppProps) {

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

export default MyApp;

page/a.tsx

export default function A() {
  return (
    <p>A</p>
  );
}

page/b.tsx

export default function B() {
  return (
    <p>B</p>
  );
}

見栄えが良くなりました。
そして、ページ遷移してもCanvasは初期化されません。

これで良いといえば良いのですが、今後Canvasを表示しないページCが出てくることもあるかもしれないので、Layoutをapp.tsxで読み込むのではなく、ページ側で読み込むようにします。

pages/app.tsx

import { NextPage } from 'next';
import { AppProps } from 'next/app';
import { ReactElement, ReactNode } from 'react';

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

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

function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  const getLayout = Component.getLayout ?? ((page) => page) // ComponentにgetLayoutが定義されてない場合は引数をそのままreturnする関数を使う

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

export default MyApp;

pages/a.tsx

import { ReactNode } from 'react';
import Layout from '../components/Layout';

export default function A() {
  return (
    <p>A</p>
  );
}

A.getLayout = function getLayout(page: ReactNode) {
  return (
    <Layout>{ page }</Layout>
  );
}

pages/b.tsx

import { ReactNode } from 'react';
import Layout from '../components/Layout';

export default function B() {
  return (
    <p>B</p>
  );
}

B.getLayout = function getLayout(page: ReactNode) {
  return (
    <Layout>{ page }</Layout>
  );
}

挙動も問題ありません。
これで、将来的にCanvasが必要ないページが出てきても安心です。