Closed15

Next.jsのdevelopment modeとproduction modeで画面遷移の挙動がちがう問題を解決したい

nus3nus3

おきたこと

useRouterのpushを使ったpage間の遷移が
Next.jsのdevelopment modeでは想定した挙動をするが
production mode(next buildしてからnext startする)の場合に画面遷移がとても遅く1回しか遷移できない。
またheadless UIなどのコンポーネントが描画されない。

nus3nus3

環境

package.json
package.json
{
 "dependencies": {
    "@headlessui/react": "1.3.0",
    "@hookform/error-message": "2.0.0",
    "@hookform/resolvers": "2.6.0",
    "axios": "0.21.1",
    "classnames": "2.3.1",
    "moment": "2.29.1",
    "next": "10.2.3",
    "next-seo": "4.26.0",
    "react": "17.0.2",
    "react-dates": "21.8.0",
    "react-dom": "17.0.2",
    "react-hook-form": "7.9.0",
    "react-icons": "4.2.0",
    "react-paginate": "7.1.3",
    "react-select": "4.3.1",
    "react-textarea-autosize": "8.3.3",
    "react-toastify": "7.0.4",
    "react-tooltip": "4.2.21",
    "yup": "0.32.9"
  },
  "devDependencies": {
    "@openapitools/openapi-generator-cli": "2.3.5",
    "@slack/web-api": "6.2.4",
    "@stoplight/prism-cli": "4.2.6",
    "@storybook/addon-essentials": "6.3.0",
    "@storybook/addon-links": "6.3.0",
    "@storybook/addon-postcss": "2.0.0",
    "@storybook/addon-storyshots": "6.3.0",
    "@storybook/react": "6.3.0",
    "@types/classnames": "2.3.1",
    "@types/jest": "26.0.23",
    "@types/node": "15.0.2",
    "@types/react-dates": "21.8.2",
    "@types/react-dom": "17.0.8",
    "@types/react-paginate": "7.1.0",
    "@types/react-select": "4.0.16",
    "@types/react-tooltip": "4.2.4",
    "@types/tailwindcss": "2.2.0",
    "@types/yup": "0.29.11",
    "@typescript-eslint/eslint-plugin": "4.28.0",
    "@typescript-eslint/experimental-utils": "4.28.0",
    "@typescript-eslint/parser": "4.28.0",
    "autoprefixer": "10.2.6",
    "babel-jest": "26.6.3",
    "cypress": "7.6.0",
    "env-cmd": "10.1.0",
    "eslint": "7.29.0",
    "eslint-config-prettier": "8.3.0",
    "eslint-plugin-fp": "2.3.0",
    "eslint-plugin-import": "2.23.4",
    "eslint-plugin-jest": "24.3.6",
    "eslint-plugin-jsx-a11y": "6.4.1",
    "eslint-plugin-prettier": "3.4.0",
    "eslint-plugin-react": "7.24.0",
    "eslint-plugin-rulesdir": "0.2.0",
    "eslint-plugin-simple-import-sort": "7.0.0",
    "husky": "6.0.0",
    "jest": "26.6.3",
    "jest-watch-typeahead": "0.6.4",
    "lint-staged": "11.0.0",
    "postcss": "8.3.5",
    "postcss-nested": "5.0.5",
    "prettier": "2.3.2",
    "redoc": "2.0.0-rc.54",
    "reg-keygen-git-hash-plugin": "0.10.16",
    "reg-notify-github-plugin": "0.10.16",
    "reg-publish-s3-plugin": "0.10.16",
    "reg-suit": "0.10.16",
    "storycap": "3.0.4",
    "tailwindcss": "2.2.4",
    "ts-node": "9.1.1",
    "typescript": "4.3.2"
  },
}
nus3nus3

正しく動いているNext.jsの別プロジェクトと比較

両方のプロジェクトでnext buildしてからnext startで確認

普通に画面遷移するプロジェクト

遷移前

遷移後

問題が起きてるプロジェクト

遷移前

useRouterのpush実行時

少し時間が経ってから画面遷移された後

nus3nus3

やったこと

tailwindcssのjitモードを使わないようにした

  • 頻繁にビルド時にFATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memoryのエラーが出るようになった
  • w-[300px]みたいな書き方をやめた(jitモード使えなくなるし必然ではある)
nus3nus3

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memoryの対応はリンクメモするの忘れたけど

  • madgeをインストールし、yarn madge --circular src --extensions ts,tsxで循環参照がないか確認→なかった
  • NODE_OPTIONS='--max-old-space-size=8086' env-cmd -e mock next build && next export ビルド時のnode.jsのオプション追加
nus3nus3

jitモードを使わないようにするとtableコンポーネントを使ったページ以外は正常に動くようになった

tableコンポーネントの実装

export type TableRow = {
  id: string
  error?: boolean
  data: {
    [fieldName: string]: ReactNode
  }
}

export type TableColumn = {
  label: string
  fieldName: string
}

export type TableProps = {
  /**
   * テーブルの行に入る値
   */
  rows: TableRow[]
  /**
   * テーブルの列に入る値
   */
  columns: TableColumn[]
}

// HACK: ループ回数多い気がする
//       パフォーマンス悪かったらリファクタする
const renderBody = (row: TableRow, columns: TableColumn[]): JSX.Element => {
  const filterColumns = columns.filter((column) => column.fieldName in row.data)

  return (
    <tr
      key={row.id}
      className={classnames(styles.row, { [styles.error]: row.error })}
    >
      {filterColumns.map(({ fieldName }) => (
        <td key={`${fieldName}-${row.id}`} className={styles.column}>
          {row.data[fieldName]}
        </td>
      ))}
    </tr>
  )
}

export const Table: FC<TableProps> = ({ rows, columns }: TableProps) => {
  return (
    <table className={styles.table}>
      <thead className={styles.header}>
        <tr>
          {columns.map(({ fieldName, label }) => (
            <th key={fieldName}>
              <Txt fontSize="text-xs" bold>
                {label}
              </Txt>
            </th>
          ))}
        </tr>
      </thead>
      <tbody className={styles.body}>
        {rows.map((r) => renderBody(r, columns))}
      </tbody>
    </table>
  )
}

セルの中身をReactNodeで受け取ってるのが悪い?

nus3nus3

tableコンポーネントの何が悪いのか問題を切り分ける

  • セルの中身をコンポーネントではなくstringを渡せば大丈夫?→ダメ
  • 表示部分の条件分岐が関係してる(propsによって表示される要素が変わる)?→ダメ
  • presenterとcontainerでコンポーネント分けてるのが悪い?→ダメ
  • 同一ファイルにpresenterとcontainer両方のコンポーネントがあるのが悪い?→正しく動いた
    • tableコンポーネントだけを呼ぶようにして、presenterを削除した
  • containerでpresenterの実装をcontainerに持ってくる→ダメ

presenterの実装に問題がある?

presensterの実装
    <section className={styles.wrap}>
      <div className={styles.top}>
        <form
          className={classnames(styles['search-wrap'], {
            [styles['show-checkbox']]: showDeletedCheckBox,
          })}
        >
          {showDeletedCheckBox && (
            <div className="flex items-center">
              <CheckboxField
                items={[
                  {
                    name: 'showDeleted',
                    label: <Txt fontSize="text-xs">解約した店舗も表示する</Txt>,
                    register: register('showDeleted'),
                  },
                ]}
              />
            </div>
          )}
          <div className={styles.search}>
            <SearchField
              register={register('searchValue')}
              onKeyPressEnter={handleSearch}
            />
          </div>
        </form>
        <Table rows={rows} columns={columns} />
      </div>
      <div className={styles.footer}>
        <Pagination {...paginationProps} />
      </div>
    </section>
nus3nus3

tableコンポーネントのpresenterの何が悪いのか

  • 表示の条件分岐が悪い?→ダメ
  • tableコンポーネント以外のコンポーネントが悪い?→ダメ
    • tableコンポーネントと素のタグ以外使わないようにしたけどダメだった
  • classnamesで条件分岐でclassNameを渡してるのが悪い?→動いた
  • もしやclassnamesの書き方がes6の書き方でやってるのにes5指定してた?
    • classnamesの条件分岐を元に戻してtsconfigのcompilerOptionsのtargetをes6にしてみる→動いた
  • table以外のコンポーネントを元に戻してみる→ダメ
  • search field コンポーネントをコメントアウト→ダメ
  • checkbox field コンポーネントをコメントアウト→ダメ
  • pagination コンポーネントをコメントアウト→ダメ
  • import classnames from 'classnames' が悪い?
  • pagination, search field, checkbox fieldを取り除いたら動いた
    • pagination追加しても動いた
    • search field追加しても動かない
    • checkbox field追加しても動かない
tableコンポーネント以外のコンポーネントが悪い
<section className={styles.wrap}>
      <div className={styles.top}>
        <form
          className={styles['search-wrap']}
        >
          <div className={styles.search}>
          </div>
        </form>
        <Table rows={rows} columns={columns} />
      </div>
    </section>
classnamesで条件分岐
        <form
          className={classnames(styles['search-wrap'], {
             [styles['show-checkbox']]: showDeletedCheckBox,
          })}
        >
nus3nus3

search fieldの何が悪いのか

  • classnames?→ダメ
  • search fieldコンポーネントの代わりにtext fieldコンポーネント使ったら動いた
search fieldコンポーネント
import classnames from 'classnames'
import { FC } from 'react'
import { UseFormRegisterReturn } from 'react-hook-form'

import { Icon } from 'components/atoms/Icon'

import styles from './styles.module.css'

export type SearchFieldProps = {
  /**
   * react-hook-form のregister('fieldName')の返り値の型
   */
  register: UseFormRegisterReturn
  /**
   * errorかどうか
   */
  error?: boolean
  /**
   * プレースホルダ
   */
  placeholder?: string
  /**
   * 初期値
   */
  defaultValue?: string
  /**
   * enterキーをpressした時のイベントハンドリング
   */
  onKeyPressEnter: () => void
}

export const SearchField: FC<SearchFieldProps> = ({
  register,
  error,
  placeholder,
  defaultValue,
  onKeyPressEnter,
}: SearchFieldProps) => {
  return (
    <div className={styles.wrap}>
      <input
        {...register}
        className={classnames(styles.input, {
          [styles.error]: error,
        })}
        type="text"
        placeholder={placeholder}
        defaultValue={defaultValue}
        onKeyPress={(e) => {
          if (e.key !== 'Enter') return

          e.preventDefault()
          onKeyPressEnter()
        }}
      />
      <span className={styles.icon}>
        <Icon name="search" color="text-textLight" fontSize="text-lg" />
      </span>
    </div>
  )
}

text fieldコンポーネント
//import classnames from 'classnames'

import { FC } from 'react'

import { InputText, InputTextProps } from 'components/atoms/forms/InputText'
import { Txt } from 'components/atoms/texts/Txt'
import { LabelBadge } from 'components/molecules/badges/LabelBadge'

import styles from './styles.module.css'

export type TextFieldProps = {
  /**
   * input textのprops
   */
  inputTextProps: InputTextProps
  /**
   * label
   */
  label: string
  /**
   * ラベルに米印をつけるかどうか
   */
  optioned?: boolean
  /**
   * 最大の文字数
   */
  maxCount?: number
  /**
   * 現在の文字数
   */
  currentCount?: number
  /**
   * 必須かどうか
   */
  required?: boolean
}

export const TextField: FC<TextFieldProps> = ({
  inputTextProps,
  label,
  optioned,
  currentCount,
  maxCount,
  required,
}: TextFieldProps) => {
  return (
    <div className={styles.wrap}>
      <div className={styles['label-wrap']}>
        <Txt fontSize="text-sm">{label}</Txt>
        <LabelBadge required={required} />
        {optioned && (
          <Txt color="text-alert" fontSize="text-sm"></Txt>
        )}
      </div>
      <InputText {...inputTextProps} />
      {currentCount !== undefined && maxCount && (
        <div className={styles['count-wrap']}>
          <Txt fontSize="text-xs">
            {currentCount} / {maxCount}
          </Txt>
        </div>
      )}
    </div>
  )
}

nus3nus3

Next.jsがどのようにビルドしてるのか追わんと同じような地雷踏みそう

nus3nus3

やったこと

  • yarn add next@latest
  • yarn add --dev eslint-config-next
    してnextのversionを11.0.1にあげた
このスクラップは2021/07/05にクローズされました