Help us understand the problem. What is going on with this article?

Next.jsを ver8.1.0からver10.0.3に上げたログ

この記事はLivesense アドベントカレンダー2020の20日目の記事です。
今期、プロダクトで利用しているNext.jsのバージョンをv8.1.0からv10.0.3まで上げたので、マイナーバージョンごとのアップグレード時の作業内容を公開しようと思います。
今後同じような作業をする方の参考になると嬉しいです。

転職会議チームはページが取り扱うドメインごとにリポジトリが分かれていて、2020年12月現在Next.jsを利用しているページの例が以下です。

企業検索ページ: https://jobtalk.jp/companies/search
企業詳細ページ: https://jobtalk.jp/companies/4075, https://jobtalk.jp/companies/4075/answers など

これらのページのフロントエンドアプリケーションがNext.jsで作成されていて、データ取得時にRails製のBFFおよびAPIサーバーと通信する、というような構成です。利用している主なライブラリのバージョンは以下の通りです。

"react": "^16.13.1",
"redux": "^4.0.5",
"redux-observable": "^1.2.0",
"typescript": "^3.8.3",

アップグレードは以下のような方針で進めました。

  • マイナーバージョンごとのアップグレードを行い都度打鍵
  • マイナーバージョンアップグレード時のパッチバージョンはその時点で最新のものを適用
  • 大幅な修正が入る対応などについてはマイナーバージョンアップグレードとは分けて対応

8.1.0 -> 9.0.8

ver 9 公式ブログ
ver 9.0.7 公式ブログ

Next.jsおよび関連ライブラリのバージョンアップグレード

package.json
{
  ...
  "dependencies": {
    ...
-    "next": "^8.1.0",
+    "next": "9.0.8",
-    "next-redux-wrapper": "^3.0.0",
+    "next-redux-wrapper": "^4.0.1", // Next.js ver9への対応
-    "@zeit/next-typescript": "^1.1.1",
  },
  "devDependencies": {
-    "@types/next": "^8.0.5", // ビルトインTSサポートにより不要化
-    "@types/next-redux-wrapper": "^2.0.2", // ビルトインTSサポートにより不要化
-    "fork-ts-checker-webpack-plugin": "^4.1.3", // ビルトインTSサポートにより不要化
  },
  ...
}

NextFC廃止 => NextPageへ

src/pages/companyDetail.tsx
import { NextPage } from 'next'

// 型をちゃんとしてないのは一旦後回し
- const CompanyPage: NextFC<PropsType, { companyId: number; isServer?: boolean }, any> = ({
+ const CompanyPage: NextPage<any> = ({
...

next/linkの外部リンクとしての使用法の廃止

ver8系以前はリポジトリ外へのリンクにnext/linkのLinkコンポーネントを利用できていましたが、9系以降は外部リンクにLinkコンポーネントを利用すると、Invalid href passed to routerというエラーが吐かれるようになりました。
(表示されるエラー: https://github.com/vercel/next.js/blob/master/errors/invalid-href-passed.md)
これを受けて、外部リンクについてはaタグを利用するように修正しました。

const LinkToGoogle: React.FC = () => (
- <Link href={'https://google.com'}>
-   <a>Google</a>
- </Link>
+ <a href="https://google.com">Google</a>
)

useRouterのジェネリクス経由でのrouter.queryの型指定を廃止

ver8系以前はnext/routerからインポートしたuseRouterに対して、ジェネリクス経由でrouter.queryの型を指定する方法が利用できました。
たとえば

import { useRouter } from 'next/router'

const QueryText: React.FC = () => {
  const query = useRouter<QueryType>().query
  return (
    <p>query.text</p>
  )
}

みたいな使い方です。
このジェネリクス経由でのクエリ型指定が廃止され、返却されるqueryの型はParsedUrlQueryという型で統一されることになりました。
型の内容は string | string[] | undefined なのでさもありなんという感じですが、「数値の文字列であることを保証したい」などの場合には型ガードなどで対応する必要がありそうです。

型ガードでクエリの型の保証をするしないに関わらず、今回のver9化ではジェネリクス経由での型指定を削除する必要があります。

-  const query = useRouter<QueryType>().query
+  const query = useRouter().query

dynamic importのloadingオプションの初期値変更に伴う修正

Next.jsのdynamic importを利用している場合、コンポーネントのロードが完了するまでの間に表示されるコンポーネントを示すloadingオプションの初期値が、() => nullに変更されました。
確か以前は() => loading...とかだった気がしますが、これを回避するためにオプションで loading: () => nullと指定していた箇所については、オプションを削除することが可能になります。

import dynamic from 'next/dynamic'

const DynamicImported = dynamic(
  () => import('src/component/DynamicImported').then(mod => mod.DynamicImported),
- { loading: () => null }
)

WithRouterPropsのimport先の変更

next/routerからインポートするwithRouterでラップしたコンポーネントに渡るPropsの型定義ですが、こちらの型定義がnext/routerからnext/dist/client/with-routerに変更されたため、インポート先を修正する必要があります。

- import { withRouter, WithRouterProps } from 'next/router'
+ import { withRouter } from 'next/router'
+ import { WithRouterProps } from 'next/dist/client/with-router'

_app.tsxにおけるContainerコンポーネント利用の廃止

表題の通りです。以下のように修正します

src/pages/_app.tsx
import React from 'react'
import App, { Container } from 'next/app'

class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props
-   return (
-     <Container>
-       <Component {...pageProps} />
-     </Container>
-   )
+   return <Component {...pageProps} />
  }
}

export default MyApp

参照: https://github.com/vercel/next.js/blob/master/errors/app-container-deprecated.md

viewPortを設定するmetaタグをpages/_documentからpages/_appコンポーネントに移行

以下のエラーそのままです。
https://github.com/vercel/next.js/blob/master/errors/no-document-viewport-meta.md

Next.js ver9の型定義に則り、Pagesコンポーネントの型を整備

Next.js ver9系のPagesコンポーネントの型定義は、概ね以下のような感じで定義できます。

import * as React from 'react'
import { NextPage, NextPageContext } from 'next'
import { connect } from 'react-redux'
import { ParsedUrlQuery } from 'querystring'


const TestPage: NextPage<PropsType, InitialPropsType> = ({
  stateItem,
  query,
  fromAppOrDocumentItem,
  initialProps
}) => (
  <div>
    {stateItem}
    {query}
    {fromAppOrDocumentItem}
    {initialProps}
  </div>
)

TestPage.getInitialProps = async ({ query }: NextPageContext) => {
  const initialProps = !!query
  return {query, initialProps}
}

type FromAppOrDocumentItem = {
  fromAppOrDocumentItem: boolean
}

type PropsType = ReturnType<typeof mapStateToProps> & FromAppOrDocumentItem & InitialPropsType

type InitialPropsType = {
  query: ParsedUrlQuery
  initialProps: boolean
}

type StatesType = {
  stateItem: string
}

const mapStateToProps = ({ stateItem }: StatesType) => ({ stateItem })

export default connect(mapStateToProps)(TestPage)

// propsの流れるイメージ図
/**
 * <App>/<Document>
 *   ↓
 * (connect Store)
 *   ↓
 *   ↓ → → → → → → → → → → → →
 *   ↓                        ↓
 *   ↓                 getInitialProps
 *   ↓                        ↓
 *   ↓ ← ← ← ← ← ← ← ← ← ← ← ←
 *   ↓
 * NextPage
 */

これに則り、既存のPagesコンポーネントの型を整備してゆきました。

DynamicRouting対応

例えばver8.1の頃は

- src/
    - pages/
        - companySearch.tsx
        - companyDetail.tsx
        - companyAnswers.tsx
        - companyAnswerDetail.tsx
        - companyJobs.tsx
        - companyOccupations.tsx
        - companyOccupationDetail.tsx

のように、ページの概要を表現した名前のファイルが同階層に並ぶpagesディレクトリがあり、加えて src/server.jsに定義するカスタムサーバーにて

src/server.js
  server.get('/companies/:id', (req, res) => {
    const id = req.params.id
    if (isValidCompanyId(id)) {
      app.render(req, res, `/companyDetail`, { companyId: id })
    } else {
      res.status(404).end()
    }
  })

のように、リクエスト先のURLを見て各pagesコンポーネントにリクエストを振り分ける、というような実装をしていました。
これを、DynamicRoutingを導入することで

- src/
    - pages/
        - companies/
            - [companyId].tsx
            - [companyId]/
                - answers.tsx
                - answers/
                    - [answerId].tsx
                - occupations.tsx
                - occupations/
                    - [occupationId].tsx
            - search.tsx

というように、URLの構造と対応するディレクトリ構造のPagesコンポーネントファイルを配置すれば、[]で囲ったパスに該当するリクエスト先URLのパスをNext.jsが解析し、Pagesコンポーネント側で router.queryからパスパラメータを引けるようになります。
こういった修正を通して、リクエストのPagesコンポーネントへの振り分けを開発スコープから外せる点を期待して、DynamicRoutingを導入することにしました。

ファイル名の変更

DynamicRoutingに対応するページをURLに対応する形にrenameします

$ mkdir src/pages/companies
$ mv src/pages/companyDetail.tsx src/pages/companies/[companyId].tsx

カスタムサーバーのパス振り分け処理の削除

カスタムサーバーからDynamicRoutingに対応するパスへの振り分け処理を削除

src/server.js
- server.get('/companies/:id', (req, res) => {
-   const id = req.params.id
-   if (isValidCompanyId(id)) {
-     app.render(req, res, `/companyDetail`, { companyId: id })
-   } else {
-     res.status(404).end()
-   }
- })

Linkコンポーネントのhrefプロパティの修正

DynamicRoutingの利用に伴い、Linkコンポーネントのプロパティの渡し方が変わります。

import Link from 'next/link'

export const CompanyDetailLink: React.FC<{companyId: number}> = ({companyId}) => {
-  const href = `/companyDetail?companyId=${companyId}`
+  const href = '/companies/[companyId]' // pagesコンポーネントファイルのファイル名を指定
  const as = `/companies/${companyId}` // パスパラメータを含んだパスを指定

  return <Link href={href} as={as}><a>link</a></Link>
}

9.0.8 -> 9.1.7

ver 9.1 公式ブログ
ver 9.1.7 公式ブログ

Next.jsのバージョンアップグレード

package.json
  "dependencies": {
    ...
-   "next": "9.0.8",
+   "next": "9.1.7",
    ...
  },

9.1.7 -> 9.3.6

ver 9.2 公式アップグレードブログ
ver 9.3 公式アップグレードブログ

※ 一度ver9.2系最新(当時)の9.2.2に上げたのですが、IE11でポリフィルが適用されないバグがあり、ver9.3系では対応されているとのことだったので、9.1.7から9.3.6に上げることにしました。

built-in CSS Support対応

転職会議の当リポジトリでは@zeit/next-cssを利用してcssのインポートを行なっていましたが、ビルトインのCSS(モジュール)のサポートが追加されたことにより、next-cssを消すことができました。

ビルトインCSSサポートに乗ると、global CSSのimportをできる箇所はpages/_app.tsxに限定されます。
なので、他の箇所でglobal CSSをimportしている場合は、pages/_app.tsxに集約する必要があります。

moduled CSSのファイル名の修正

cssモジュール用の設定が不要になる一方で、Next.jsのビルトインCSSサポート上でcss moduleを利用しようとすると、ファイル名を{任意のファイル名}.module.cssという形式にする必要があります。
ワンライナースクリプト等で一括変更できればよかったのですが、パッとわからなかったので330ファイルほどを手作業でrenameしました。

next.config.jsからCSSモジュール用の設定を削除

next.config.js
- const withCss = require('@zeit/next-css')
...

- const cssConfig = [
-   withCss,
-   {
-     cssModules: true,
-     cssLoaderOptions: {
-       importLoaders: 1,
-       localIdentName: '[local]___[hash:base64:5]'
-     }
-   }
- ]
...

module.exports = withPlugins(
- [cssConfig, withOptimizedImages],
+ [withOptimizedImages],
  nextConfig
)

Next.jsのバージョンアップグレードおよびnext-cssへの依存削除

package.json
  "dependencies": {
    ...
-   "@zeit/next-css": "^1.0.1",
-   "next": "9.1.7",
+   "next": "9.3.6",
    ...
  },

9.3.6 -> 9.4.4

ver 9.4 公式ブログ

Next.jsのバージョンアップグレード

package.json
  "dependencies": {
    ...
-   "next": "9.3.6",
+   "next": "9.4.4",
    ...
  },

9.4.4 -> 9.5.5

ver 9.5 公式ブログ

next/linkのasプロパティの利用方法変更への対応

ver9.5.3からhrefに指定したパスを解析してpages/配下のファイルパスに当てはめてくれるようになったので、hrefに明示的にファイルパスを渡す必要がなくなりました。ただこの変更は後方互換性があるので、対応しなくてもバージョンを上げることは可能です。

// pages/companies/[companyId].tsx というファイルがある場合
- <Link href={"/companies/[companyId]"} as={"/companies/4075"}>
+ <Link href={"/companies/4075"}>
    <a>リブセンス</a>
  </Link>

外部ドメインのCDNからアセットを取得する際に、arrow-originの設定がされていてもCORSエラーになるバグへの対応

報告されているissue にあるコメント通り、next.config.jsにcrossOriginプロパティを追加します。

next.config.js
const nextConfig = {
  ...
  crossOrigin: 'anonymous',
  ...
}

Next.jsのバージョンアップグレード

package.json
  "dependencies": {
    ...
-   "next": "9.4.4",
+   "next": "9.5.5",
    ...
  },

9.5.5 -> 10.0.3

ver 10 公式ブログ

Next.jsのバージョンアップグレード

package.json
  "dependencies": {
    ...
-   "next": "9.5.5",
+   "next": "10.0.3",
    ...
  },

おわりに

以上でNext.jsのバージョンアップグレードは完了です。
next/imageやgetInitialProps -> getServerSidePropsへの移行、部分的なSSG、React17への対応など、まだ利用しきれていない機能も多いので、これからいろいろ試してみたいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away