この記事は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
Next.jsおよび関連ライブラリのバージョンアップグレード
{
...
"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へ
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コンポーネント利用の廃止
表題の通りです。以下のように修正します
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
に定義するカスタムサーバーにて
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に対応するパスへの振り分け処理を削除
- 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
Next.jsのバージョンアップグレード
"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モジュール用の設定を削除
- 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への依存削除
"dependencies": {
...
- "@zeit/next-css": "^1.0.1",
- "next": "9.1.7",
+ "next": "9.3.6",
...
},
9.3.6 -> 9.4.4
Next.jsのバージョンアップグレード
"dependencies": {
...
- "next": "9.3.6",
+ "next": "9.4.4",
...
},
9.4.4 -> 9.5.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プロパティを追加します。
const nextConfig = {
...
crossOrigin: 'anonymous',
...
}
Next.jsのバージョンアップグレード
"dependencies": {
...
- "next": "9.4.4",
+ "next": "9.5.5",
...
},
9.5.5 -> 10.0.3
Next.jsのバージョンアップグレード
"dependencies": {
...
- "next": "9.5.5",
+ "next": "10.0.3",
...
},
おわりに
以上でNext.jsのバージョンアップグレードは完了です。
next/imageやgetInitialProps -> getServerSidePropsへの移行、部分的なSSG、React17への対応など、まだ利用しきれていない機能も多いので、これからいろいろ試してみたいと思います。