Next.jsのAppとDocumentとページはどんな順序で実行されるのか調べてみた

先日commmuneの_app.tsxの整理をしました。 そのときに_app.tsxと_document.tsxとページコンポーネントがどんな順序で実行されるのか混乱したので調べてみました。

AppコンポーネントとDocumentコンポーネントとは

Appはすべてのページコンポーネントの初期化に使われるコンポーネントです。 全ページに必要な処理はAppコンポーネントに書くことで実装できます。 Appコンポーネントは./page/_app.jsにファイルを作ることでカスタマイズできます。

Documentはhtmlタグやbodyタグの定義を行うコンポーネントです。 全ページ共通でheadタグ内で読み込みしたい場合などはDocumentコンポーネントをカスタマイズすることで実装します。 Documentコンポーネントは./page/_document.jsにファイルを作ることでカスタマイズできます。 また、Documentコンポーネントはブラウザでは実行されません。サーバサイドでのみ実行されます。

SSRで実行されるメソッド

Next.jsではSSR用にgetServerSidePropsとgetInitialPropsの2つのメソッドが提供されています。 これらのメソッドをページコンポーネントに実装することで、サーバサイドで実行される処理を作ることができます。

  • getServerSideProps:必ずサーバサイドで実行される。getServerSidePropsが実装されたページにアクセスするときは、サーバへ問い合わせが走る。v9.3以降で使える。
  • getInitialProps:ブラウザ側でも実行される。URLから直リンクでアクセスした場合などサーバへ問い合わせがあるときはサーバサイドで実行される。

また、AppコンポーネントやDocumentコンポーネントではgetInitialPropsを使うことはできますが、getServerSidePropsは使うことができません。

最新のNext.jsではgetInitialPropsよりもgetServerSidePropsを使うことが推奨されています。 commmuneはNext.jsのv9.3がリリースされる以前から開発されており、まだgetInitialPropsを使ったコードが残っています。 今後開発するなかで徐々にgetServerSidePropsへ移行していきたいと考えています。

検証方法

ブラウザからアクセスしたときに、カスタムApp, Document, ページのそれぞれのコンポーネントでログを出力することよって検証します。 各コンポーネントの以下の場所でログを出力しました。

  • カスタムApp:getInitialProps, 関数コンポーネント
  • カスタムDocument:getInitialProps
  • ページコンポーネント:getInitialProps, getServerSideProps, 関数コンポーネント

カスタムAppではgetInitialPropsメソッド内のApp.getInitialPropsの前後2箇所で出力しています。

getInitialPropsメソッドを実装したgetInitialPropsページ、getServerSidePropsメソッドを実装したgetServerSidePropsページの2種類のページにアクセスしてログを出力させます。 また、URLから直リンクでアクセスする場合とリンクから遷移してアクセスする場合の2パターンで検証します。

検証用コード

_app.tsx

import App from 'next/app'
import type { AppProps ,AppContext } from 'next/app'

function MyApp({ Component, pageProps }: AppProps) {
    console.log('_app: component')

  return <Component {...pageProps} />
}

MyApp.getInitialProps = async (appContext: AppContext) => {
    console.log('_app: getInitialProps1')
  const appProps = await App.getInitialProps(appContext);
    console.log('_app: getInitialProps2')

  return { ...appProps}
}

export default MyApp

_document.tsx

import Document, { DocumentContext } from 'next/document'

class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const initialProps = await Document.getInitialProps(ctx)

        console.log('_document: getInitialProps')

    return initialProps
  }
}

export default MyDocument

getInitialProps.tsx

import React from 'react'

const GetInitialPropsPage = () => {
    console.log('page: component')
    return (
        <p>getInitialProps</p>
    )
}

GetInitialPropsPage.getInitialProps = async () => {
    console.log('page: getInitialProps')
    return {}
}

export default GetInitialPropsPage

getServerSideProps.tsx

import React from 'react'

const GetServerSidePropsPage = () => {
    console.log('page: component')
    return (
        <p>getServerSideProps</p>
    )
}

export async function getServerSideProps() {
    console.log('page: getServerSideProps')
    return { props: {}}
}

export default GetServerSidePropsPage

index.tsx (リンク遷移用のページ)

import Link from 'next/link'

const IndexPage = () => (
  <>
    <p>
      <Link href="/getInitialProps">
        <a>getInitialProps</a>
      </Link>
    </p>
    <p>
      <Link href="/getServerSideProps">
        <a>getServerSideProps</a>
      </Link>
    </p>
  </>
)

export default IndexPage

検証結果

検証したところサーバ側とブラウザ側で次のようなログが得られました。

getInitialPropsページにアクセス

直アクセス
// サーバーサイドログ
_app: getInitialProps1
page: getInitialProps
_app: getInitialProps2
_app: component
page: component
_document: getInitialProps
// ブラウザログ
_app: component
page: component
リンク遷移
// サーバーサイドログ
// なし
// ブラウザログ
_app: getInitialProps1
page: getInitialProps
_app: getInitialProps2
_app: component
page: component

getServerSidePropsページにアクセス

直リンクアクセス
// サーバーサイドログ
_app: getInitialProps1
_app: getInitialProps2
page: getServerSideProps
_app: component
page: component
_document: getInitialProps
// ブラウザログ
_app: component
page: component
リンクから遷移
// サーバーサイドログ
_app: getInitialProps1
_app: getInitialProps2
page: getServerSideProps
// ブラウザログ
_app: component
page: component

まとめ

検証結果から以下の順序でコードが実行されることがわかりました。

  1. カスタムAppのgetInitialPropsメソッド内のApp.getInitialPropsの前に書かれたコード
  2. ページコンポーネントのgetInitialPropsメソッド
  3. カスタムAppのgetInitialPropsメソッド内のApp.getInitialPropsの後ろに書かれたコード
  4. ページコンポーネントのgetServerSidePropsメソッド
  5. カスタムAppの関数コンポーネント内のコード
  6. ページコンポーネントの関数コンポーネント内のコード
  7. カスタムDocumentのgetInitialPropsメソッド

これで実行順序に混乱することも無さそうです!