先日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
まとめ
検証結果から以下の順序でコードが実行されることがわかりました。
- カスタムAppのgetInitialPropsメソッド内の
App.getInitialProps
の前に書かれたコード - ページコンポーネントのgetInitialPropsメソッド
- カスタムAppのgetInitialPropsメソッド内の
App.getInitialProps
の後ろに書かれたコード - ページコンポーネントのgetServerSidePropsメソッド
- カスタムAppの関数コンポーネント内のコード
- ページコンポーネントの関数コンポーネント内のコード
- カスタムDocumentのgetInitialPropsメソッド
これで実行順序に混乱することも無さそうです!