LoginSignup
8

More than 1 year has passed since last update.

posted at

updated at

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への対応など、まだ利用しきれていない機能も多いので、これからいろいろ試してみたいと思います。

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
What you can do with signing up
8