54
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Next.jsAdvent Calendar 2019

Day 21

AppComponentの処理を型安全に行う

Last updated at Posted at 2019-12-20

この記事は Next.js Advent Calendar 2019 の21日目の記事です。

Next.js はフレームワークとして必要最低限の機構のみ提供しているため、運用上必要になる機構は自作する必要があります。例えば全ページで「メタデータの注入」が必要になったとき、それを制約する機構は備わっていません。本稿では、middleware とも呼べる AppComponent でこの受領するメタデータを型で守り、処理を安全に実行するTIPSを紹介します。

Next.js のバージョンは次のものです。

package.json
"next": "^9.1.6"

求めるコードベースの確認

Title の指定が簡略化できる

ページタイトルは必須になりますので、getInitialProps で文字列を返すだけでタイトルが指定出来れば便利です。Page毎に"next/head"を import せずにすみます。

pages/index.tsx
import Component from "../components/index"

const PageComponent = () => <Component />

PageComponent.getInitialProps = async () => {
  // titleの設定はこれだけだと幸せ
  return { title: "TOP" }
}

_app.tsxが全ページの高階コンポーネントになるので、次の様な AppComponent にしてしまえば実現できます。この様に各pageではメタデータを返すのみとし、取り扱いを一元管理できれば、柔軟な拡張を施すことができます。

pages/_app.tsx
import Head from "next/head"

const AppComponent = ({ Component, pageProps }) => (
  <>
    <Head>
      <title>{pageProps.title}</title>
    </Head>
    <Component {...pageProps} />
  </>
)

全ページに非同期処理を挟める

ページタイトル以外にも、全ページで共通の非同期処理を行う必要がある場合。これも同じく実現する場所は_app.tsxです。次のように、全ページでログの送信を試みます。

pages/_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で注入します。この型付与が行われていれば、戻り値が期待値を満たしていないページがある場合、コンパイルエラーを得ることができます。

pages/index.tsx
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 宣言します。これで、パッケージから提供されている型の様に扱うことが出来ます。

types/index.ts
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 から提供されている型です。

pages/_error.tsx
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が付与されてしまっています。

pages/_app.tsx
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が付与されたpagePropsOmitで一度削り、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型に手を入れてしまえば、あとはコンパイラが修正すべき箇所を教えてくれます。サンプルコードは以下に起きましたので、覗いてみてもらえると嬉しいです。

54
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?