この記事は Next.js Advent Calendar 2019 の21日目の記事です。
Next.js はフレームワークとして必要最低限の機構のみ提供しているため、運用上必要になる機構は自作する必要があります。例えば全ページで「メタデータの注入」が必要になったとき、それを制約する機構は備わっていません。本稿では、middleware とも呼べる AppComponent でこの受領するメタデータを型で守り、処理を安全に実行するTIPSを紹介します。
Next.js のバージョンは次のものです。
"next": "^9.1.6"
求めるコードベースの確認
Title の指定が簡略化できる
ページタイトルは必須になりますので、getInitialProps で文字列を返すだけでタイトルが指定出来れば便利です。Page毎に"next/head"
を import せずにすみます。
import Component from "../components/index"
const PageComponent = () => <Component />
PageComponent.getInitialProps = async () => {
// titleの設定はこれだけだと幸せ
return { title: "TOP" }
}
_app.tsx
が全ページの高階コンポーネントになるので、次の様な AppComponent にしてしまえば実現できます。この様に各pageではメタデータを返すのみとし、取り扱いを一元管理できれば、柔軟な拡張を施すことができます。
import Head from "next/head"
const AppComponent = ({ Component, pageProps }) => (
<>
<Head>
<title>{pageProps.title}</title>
</Head>
<Component {...pageProps} />
</>
)
全ページに非同期処理を挟める
ページタイトル以外にも、全ページで共通の非同期処理を行う必要がある場合。これも同じく実現する場所は_app.tsx
です。次のように、全ページでログの送信を試みます。
AppComponent.getInitialProps = async (appContext) => {
const appProps = await App.getInitialProps(appContext)
// 全ページにメタデータを要求し、
// ロギングなどの middleware非同期処理を挟みたい。
const { logData } = appProps.pageProps
// ロギングAPIなどをここで叩く
await log(logData)
return { ...appProps }
}
課題の確認
このままでは、各pageコンポーネントの getInitialProps 返り値に不整合があった場合、事故に繋がります。ロギングAPI の payload が、将来的に変更になることも想定できます。これを型定義で要求する様に変更していきます。
型制約を設け全ページで要求する
必要最低限のプロパティを表す型定義です。全ページの getInitialProps 返り値にこれを要求します。
type PageProps = {
title: string
logData: {
screenName: string
}
}
Next.js にバンドルされているNextPage
型を利用し、Genericsで注入します。この型付与が行われていれば、戻り値が期待値を満たしていないページがある場合、コンパイルエラーを得ることができます。
import { NextPage } from "next"
import { PageProps } from "../types"
import Component from "../components/index"
const PageComponent: NextPage<PageProps> = () => (
<Component />
)
PageComponent.getInitialProps = async () => {
// 戻り値が期待値を満たしていない場合、コンパイルエラーを得られる
return {
title: "TOP",
logData: { screenName: 'TOP' }
}
}
要求を一限管理する
全ページに求める記述ですので、冗長な import を簡略化したいです。次のとおり変更していきます。
- import { NextPage } from "next"
- import { PageProps } from "../types"
+ import { PageFC } from "next"
- const PageComponent: NextPage<PageProps> = () => (
+ const PageComponent: PageFC = () => (
PageFC
型は、NextPage
型を拡張した独自定義型です。必要な拡張だけを施し、Generics で各ページ固有のプロパティ注入ができる振る舞いはそのままとします。
type PageFC<P = {}, IP = P & PageProps> = NextPage<P, IP>
ここまでの型を、Ambient Module 宣言します。これで、パッケージから提供されている型の様に扱うことが出来ます。
import "next"
declare module "next" {
// 全ページ getInitialProps 戻り値に要求する、必須プロパティ
export type PageProps = {
title: string
logData: {
screenName: string
}
}
// PageComponent に適用する型
export type PageFC<P = {}, IP = P & PageProps> = NextPage<P, IP>
}
_error.tsx に適用してみる
試しに_error.tsx
に適用してみましょう。ErrorProps
型は Next.js から提供されている型です。
import React from "react"
import { PageFC } from "next"
import { ErrorProps } from "next/error"
const ErrorComponent: PageFC<ErrorProps> = ({ statusCode }) => (
<>
{statusCode
? `An error ${statusCode} occurred on server`
: "An error occurred on client"}
</>
)
ErrorComponent.getInitialProps = async ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404
return {
statusCode,
title: `Error | ${statusCode}`,
logData: { screenName: "Error" }
}
}
export default ErrorComponent
AppComponent の引数型注釈
これだけでは、AppComponent でタイトル注入を行う際に問題が発生します。Next.js から提供されているAppProps
型は、肝心なpageProps
に対しany
が付与されてしまっています。
import Head from "next/head"
import { AppProps } from "next/app"
const AppComponent = ({ Component, pageProps }: AppProps) => (
<>
<Head>
{/** var pageProps: any */ }
<title>{pageProps.title}</title>
</Head>
<Component {...pageProps} />
</>
)
この問題は、any
が付与されたpageProps
をOmit
で一度削り、PageProps
型を再付与することで、解決出来ます。独自定義型AppPageProps
を新たに追加します。
export type AppPageProps = Omit<AppProps<PageProps>, "pageProps"> & {
pageProps: PageProps
}
次の様に書くこともできます。Override
型を用意しておけば、本家の型定義にany
が付与されて困った時に便利です。(元の型を書き換えることはできません)
type Override<T extends U, U> = Omit<T, keyof U> & U
export type AppPageProps = Override<
AppProps<PageProps>,
{ pageProps: PageProps }
>
もし運用中に何か必須項目を増やす必要がでた場合、PageProps
型に手を入れてしまえば、あとはコンパイラが修正すべき箇所を教えてくれます。サンプルコードは以下に起きましたので、覗いてみてもらえると嬉しいです。