• ..

Sass

    getInitialProps と react-redux の connect で渡す props は後者優先

    以下では、getInitialPropsfoo: 1という値で返しているので、this.props.fooには1が入ってきます。が、以下のようにconnectでバインド同じプロパティに対してバインドしてしまうとconnectが優先されてしまいます。

    class FooBase extends React.Component {
      static getInitialProps() {
        return {
          foo: 1
        }
      }
    
      render() {
        console.log(this.props.foo)
        // output: 9
    
        return <div>foo</div>
      }
    }
    
    connect(() => ({foo: 9})(FooBase)

    少し考えれば当たり前な気もしますが、ちょっとハマりました。ちょっとだけ名前を工夫するか、、というより構造を見直す?

    サーバーサイドでのみデータを通信して取得後、クライアントサイドでもそれを使いレンダリング

    getInitialPropsスタティックメソッドは React のライフサイクルのメソッド群のようにサーバーサイドでもクライアントサイドでも呼び出されます。サーバーサイドで返した値はwindow.__NEXT_DATA__から取得できるので、クライアントサイドではここから値を取得することで通信なしでいける場合があります。

    req か res を使ってサーバーサイドでのみ実行される処理を書く

    このgetInitialPropsに渡ってくるコンテキストのreqresというプロパティはサーバーサイドだけでしか渡ってこない値です。ですのでこれを使ってサーバーサイド判定ができます。

    以下は例コードです。reqがある場合はサーバーサイドなのでデータを取得する処理を行い、ない場合(クライアントサイド)ではサーバーサイドで返した値をただ渡すことでクライアントサイドでの通信を無くしています。。

    class extends React.Component {
      static async getInitialProps({req}) {
        let data;
        if (req) {
          data = await axios.get('https://xxx.com/xxx')
        } else {
          data = window.__NEXT_DATA__.props.pageProps.data;
        }
    
        return {data};
      }
    }

    ターミナルログを非表示にする

    やりたいこと

    SSR 中のログや、別の所から出したログなどが NextJS ビルとインの(リフレッシュと)ログで消されてしまうので一時的に NextJS のログを表示されないようにしたい。
    ちなみにこれのこと。

     [DONE]  Compiled successfully in 2090ms
     [WARNING]  Compiled with 1 warnings

    解決

    このログは geowarin/friendly-errors-webpack-plugin というプラグインを使って出しているので、このプラグインを取り除きます。
    next.config.jswebpack で設定を以下のように変えます。

    const path = require('path');
    
    module.exports = {
      webpack(config, {dev}) {
        if (dev) {
          config.plugins = config.plugins.filter(
            plugin => !(plugin.constructor.name === 'FriendlyErrorsWebpackPlugin')
          );
        }
    
        return config;
      }
    };

    Polyfil ファイルを Webpack の entry に含める

    例えばこのサイトだとIntersectionObserverを使っていて、これはスマホなどでまだ対応しておらず見れないことが多いので含めています。

    なぜ Webpack の entry に含めたいのか

    <script>での読み込みは Lighthouse のスコアを下げる

    HTMLで以下の Polyfil を読み込むことで、IntersectionObserverを対応していないサイトでも使えるようにできます。

    <script src="https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script>

    しかしこれは、Minimize Critical Requests Depthとして引っかかってしまいます。

    NextJS の entry ファイルのルートで Polyfil を読み込む

    上のnpm版である、intersection-observerを使います。

    Polyfil は基本エントリーの最初に読み込むファイルの先頭にimportを記載すれば使えるので、_app.[jt]sx?をその形にすれば使えそうです。しかし、この Polyfil は中で window オブジェクトへアクセスしている部分があり NextJS ではその_appファイルも SSR で使われるので、 「windowundefinedだよ」なエラーが起こってしまうのでこれも駄目です。

    // _app.tsx
    import 'intersection-observer';

    NextJS の Webpack-Config の entry に含める

    next.config.jsからwebpack設定を自身の設定で上書きする方法です。

    next.config.jsのデフォルトentry() => Promise<{[x: string]: string[]}>な形をしていて、このオブジェクトを上書きすれば良さそうです。以下は戻り値の例です。

    Promise {
      { 'main.js': [],
        'static/runtime/main.js':
         [ '/Users/nju33/github/blog/node_modules/next/dist/client/next-dev' ],
        'static/development/pages/_app.js': [ './pages/_app.tsx' ],
        'static/development/pages/_error.js':
         [ '/Users/nju33/github/blog/node_modules/next/dist/pages/_error.js' ] } }

    このmain.jsへ含めてます。

    {
      webpack: config => {
        const originalEntry = config.entry;
        config.entry = () => {
          return originalEntry().then(entry => {
            entry['main.js'] = 'intersection-observer';
            return entry;
          }
        }
      }
    }

    ただまだこれだと、サーバーサイド用のビルドファイルにもintersection-observer Polyfil が含まれてしまい、上記と同じエラーが起きてしまうので、含めるのは Client 用のビルドファイルだけにする必要があります。
    幸いにも、next.config.jswebpack関数は第二引数にoptions.isServerというフラグを渡してくれます。これは Client 用のビルド時にはfalseになってくれるフラグです。

    つまり、

    • options.isServertrueのときは、デフォルトのままのentryを返す
    • falseのときは、intersection-observerを含める

    とすれば解決できそうです。config.entryを以下のように改良してみます。

    {
      webpack: (config, {isServer}) => {
        const originalEntry = config.entry;
        config.entry = () => {
          if (isServer) {
            return originalEntry;
          }
    
          return originalEntry().then(entry => {
            entry['main.js'] = 'intersection-observer';
            return entry;
          }
        }
      }
    }

    上のように変更したところ Lighthouse のスコアがあがり、かつIntersectionObserverに対応していない端末も対応することができました。

    Head の chilren として title タグを含む SFC を入れても title 要素にテキストが入らない場合

    問題

    こんなSFCを用意してました。

    export const Meta = ({title}) => {
      return (
        <>
          <meta name="twitter:title" content={title} />
          <meta property="og:title" content={title} />
          <title>{title}</title>
        </>
      );
    };
    

    title: 'foo'として展開後こんな感じで入ってくれません。。

    <meta name="twitter:title" content="foo" />
    <meta property="og:title" content="foo" />
    <title></title>

    解決

    とりあえず直下に持っていけば大丈夫でした。

    <Head>
      <Meta title={title} />
      <title>{title}</title>
    </Head>

    Head コンポーネントの中身は頭に追加される

    問題

    例えば_document.jsx<head>のデフォルトを定義して各ページでそれを上書きしようとしてもできないものがある。

    _document.jsx<head>

    <head>
      <title>foo</title>
    </head>

    pages/index.jsx<Head>

    const Head = require('next/head');
    
    <Head>
      <title>bar</title>
    <Head>

    結果

    <title>bar</title>
    <title>foo</title>

    解決

    ページ毎にすべて<Head>に書く。
    まぁ<title>は何かうまくやってくれるみたいですが、ツイッターカードとかはうまく取得できてませんでした。

    commonjs で esnext なパッケージを一緒に使う

    対象

    cjs で書かれている。つまり以下のように書かれてるもの。例えば、sindresorhus/ky

    module.exports = /* ... */

    esnext なobject-rest-spread-propertiesなどが使われてる。

    // 例: ky/index.js
    returnValue = {...returnValue, [key]: value};

    問題

    • 対象のファイルを Babel で変換しないとエラー
    • 依存のnext-babel-loaderの中でnextが読み込む.babelrcの設定がmodules:falseなので変換できても cjs が解決できない
    • next.config.js上で Babel の設定を上書きできない
    • .babelrcでは処理的に 1 つの設定しか持てない
    • 自分のソースコードは cjs にしたくない
      webpack(config, {isServer}) {
        if (isServer) {
          return config;
        }
    
        config.module.rules.map(rule => {
          if (
            rule.test.test('.js') &&
            Object.prototype.hasOwnProperty.call(rule, 'exclude') &&
            rule.exclude.test('node_modules')
          ) {
            rule.exclude = /node_modules\/(?!ky)/;
            // Module build failed: Error: Duplicate plugin/preset detected.
            rule.options = {
              presets: ['next/babel', {
                'preset-env': {
                  modules: 'commonjs'
                },
              }]
            }
          }
          return rule;
        });
    
        return config;
      },

    解決

    やっぱりこれだけにルール追加するぐらいしかなかった。(読み込んでるプラグインなどはデフォルトの設定と同じもの)

      webpack(config, {isServer}) {
        if (isServer) {
          return config;
        }
    
        config.module.rules.unshift({
          test: /\.jsx?$/,
          include: /node_modules\/ky/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
              plugins: [
                '@babel/plugin-proposal-class-properties',
                '@babel/plugin-proposal-object-rest-spread',
                '@babel/plugin-transform-runtime',
              ],
            },
          },
        });
    
        return config;
      },

    NextJSでブラウザバック時にエラーでページ移動しない時があるので応急処置する

    環境

    • next@^6.1.1

    追記(2019-01-13)

    next@^7にしたら治ったかもしれない…?

    確認した事例

    ブラウザバックした時に時々でる。特に最初にアクセスしたページに移動する時は9割ぐらい起きてると思う。 ログはこんな感じ。

    router.js:533 Uncaught (in promise) TypeError: Cannot read property 'split' of undefined
        at Router.onlyAHashChange (router.js:533)
        at Router._callee2$ (router.js:269)
        at tryCatch (runtime.js:62)
        ...

    該当箇所のコードはこうなっている。

        key: "onlyAHashChange",
        // どうやらここで渡ってきている`as`が`undefined`なせいで、
        // その先の`as.split`の呼び出しで起きてる模様
        value: function onlyAHashChange(as) 
          // 略
    
          var _as$split = as.split('#'),
              _as$split2 = (0, _slicedToArray2.default)(_as$split, 2),
              newUrlNoHash = _as$split2[0],
              newHash = _as$split2[1]; // Makes sure we scroll to the provided hash if the url/hash are the same

    色々調べたがさっぱり分からないので応急処置

    応急処置というのは、上のasnext/routerRouter.beforePopStateで見ることができるので、もしundefinedだったらJavaScriptでページ再読込するというものです。SPAなのにリロードさせちゃうし正直微妙ですが、何も起きないよりはマシです。

    どこか_app.jsxやネストの浅い所コンポーネントのcomponentDidMountかどこかで以下のように書きます。

    Router.beforePopState(({as}) => {
      if (as === undefined) {
        window.location.href = '/';
        return false;
      }
    
      return true;
    });

    うーん、誰か直せた人いたら教えてくださいー;

    更新(2018-09-01)

    asundefinedだったらページを更新せずにURLだけ/に書き換え。

    Router.beforePopState(({as}) => {
      if (as === undefined) {
        Router.push('/', '/', { shallow: true })
        return false;
      }
    
      return true;
    });

    スマートフォンのブラウザで表示されない

    とりあえず、以下で直った。

    <script async src='https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.23.0/polyfill.min.js' />

    .babelrcの設定とかもちょっと調べたけどよく分からず。
    時間ある時にまた調べよう。

    追記

    IntersectionObserverに対応していないのも原因だった。
    僕の環境だけかもしれないけど、エラーも何も表示してくれなくてすごい分かりづらい。BrowserStackも試したけど別の変なエラーがでただけで役に立たなかった。

    追記(2019-01)

    Polyfil ファイルを Webpack の entry に含める

    起こったエラーメモ

    UnhandledPromiseRejectionWarning: Error: ENOENT: no such file or directory, open '//../../.svgo.yml'

    (node:1) UnhandledPromiseRejectionWarning: Error: ENOENT: no such file or directory, open '//../../.svgo.yml'
        at Object.fs.openSync (fs.js:646:18)
        at Object.fs.readFileSync (fs.js:551:33)
        at e.exports (/var/task/.next/server/static/iqdZhcAsT3LfSmq39Ilkz/pages/_document.js:206:307121)
        at new u (/var/task/.next/server/static/iqdZhcAsT3LfSmq39Ilkz/pages/_document.js:18:123)
        at TPFN.t.default.r.default.plugin (/var/task/.next/server/static/iqdZhcAsT3LfSmq39Ilkz/pages/_document.js:20:223985)
        at n (/var/task/.next/server/static/iqdZhcAsT3LfSmq39Ilkz/pages/_document.js:20:50105)
        at /var/task/.next/server/static/iqdZhcAsT3LfSmq39Ilkz/pages/_document.js:206:21288
        at <anonymous>

    やろうとしたこと

    now(2)へのデプロイ

    多分わかったこと

    svgo 付近のパス周りで__dirnameundefinedなのが原因かも

    修正したこと

    postcsscssnano_document.tsxで使っていたのでこれをやめて、さらにpostcsscssnanoをアンインストールして依存から外した。これでsvgo依存も無くなり表示できるようになった。

    TypeError: r.isMemo is not a function

    やろうとしたこと

    now(2)へ@now/nextでのデプロイ

    修正したこと

    https://github.com/zeit/next.js/issues/5750#issuecomment-451607604のとおりに修正で治りました。

    Error: pages/index.js from Terser

    修正したこと

    next.config.jsの Webpack の設定で EnvironmentPluginNODE_ENV=production と上書きしていたので、これをやめた。

    TypeError: Cannot read property 'minify' of undefined

    いつ

    2019-02-04

    やったこと

    terser@3.14.1を指定する

    yarn add terser@3.14.1

    package.jsonresolutions

    {
      "terser": "3.14.1"
    }