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

この記事は[Livesense アドベントカレンダー2020](https://qiita.com/advent-calendar/2020/livesense)の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 公式ブログ](https://nextjs.org/blog/next-9)
[ver 9.0.7 公式ブログ](https://nextjs.org/blog/next-9-0-7)

## Next.jsおよび関連ライブラリのバージョンアップグレード
```diff: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へ
```diff: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タグを利用するように修正しました。

```diff
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`の型を指定する方法が利用できました。
たとえば

```tsx
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化ではジェネリクス経由での型指定を削除する必要があります。

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

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

```diff
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`に変更されたため、インポート先を修正する必要があります。

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

## _app.tsxにおけるContainerコンポーネント利用の廃止
表題の通りです。以下のように修正します

```diff: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コンポーネントの型定義は、概ね以下のような感じで定義できます。

```tsx
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`に定義するカスタムサーバーにて

```javascript: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します

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

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

```diff: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コンポーネントのプロパティの渡し方が変わります。

```diff
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 公式ブログ](https://nextjs.org/blog/next-9-1)
[ver 9.1.7 公式ブログ](https://nextjs.org/blog/next-9-1-7)

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

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

# 9.1.7 -> 9.3.6
[ver 9.2 公式アップグレードブログ](https://nextjs.org/blog/next-9-2)
[ver 9.3 公式アップグレードブログ](https://nextjs.org/blog/next-9-3)

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

## built-in CSS Support対応
転職会議の当リポジトリでは[@zeit/next-css](https://github.com/vercel/next-plugins/tree/master/packages/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モジュール用の設定を削除
```diff: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への依存削除

```diff: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 公式ブログ](https://nextjs.org/blog/next-9-4)

## Next.jsのバージョンアップグレード
```diff:package.json
  "dependencies": {
    ...
-   "next": "9.3.6",
+   "next": "9.4.4",
    ...
  },
```

# 9.4.4 -> 9.5.5
[ver 9.5 公式ブログ](https://nextjs.org/blog/next-9-5)

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

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

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

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

[報告されているissue](https://github.com/vercel/next.js/issues/17029) にある[コメント](https://github.com/vercel/next.js/issues/17029#issuecomment-719535199)通り、`next.config.js`にcrossOriginプロパティを追加します。

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

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

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

# 9.5.5 -> 10.0.3
[ver 10 公式ブログ](https://nextjs.org/blog/next-10)

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

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

# おわりに

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