Gatsbyのv1からv2への移行について (1)

Gatsby Casper Stearter v1.0.7 ベースで 長らく放置していた間に Gatsby v2がリリースされてしまった

アップデートついでに結構手間かかったのでメモ。 コードと文章は公式の移行ドキュメント Migrating from v1 to v2 がベースなものの大分省略してるので、細かい仕様や破壊的変更の理由などは公式ドキュメントを読むこと

Index

  • 依存パッケージのアップデート

    • Gatsby のアップデート
    • Gatsby 関連パッケージのアップデート
    • React と ReactDOM のインストール
    • Gatsby プラグインの peerDependencies のインストール
  • 破壊的変更の対応

    • Layout コンポーネントの削除 or リファクタ
      1. Layout コンポーネントの子 (children) の実行をやめる [必須]
      1. layouts/index.jsxsrc/components/layout.jsx に移動 [必須ではないが推奨]
      1. page/templateに Layout Component をimportして適用する
      1. history, location, match をpropで渡す
      1. queryStaticQuery を使うようにする
    • navigateTonavigate に変更する
    • module周りの記述を CommonJS か ES6 どちらかに寄せる
    • .babelrc を移動させる
    • PostCSS の plugin設定を追加する
      1. 依存パッケージのインストール
      1. gatsby-plugin-postcssgatsby-config.js に追加する
      1. 各PostCSS pluginをincludeした postcss.config.js を置く
    • ReactRouter から @reach/router への移行
    • <Link>to propを String にする
    • state を渡すときは state prop を使う
    • pageに渡ってた history propの代わりに @reach/routernavigate を使う
    • <Link> の廃止になったpropsの対応
    • active時などのスタイル適用は getProps を使う
    • クライアントサイドルーティングに * を使用する
    • その他 ReactRouter のクライアントルーティングの移行

依存パッケージのアップデート

Gatsby のアップデート

最初は Gatsby 自体をアップデート

$ npm i gatsby@latest

Gatsby 関連パッケージのアップデート

gatsby- prefix なパッケージたちもアップデートする必要があるので、それを洗い出す

$ npm outdated
Package                          Current   Wanted   Latest  Location
babel-eslint                       8.2.6    8.2.6   10.0.1  gatsby-starter-casper
eslint                             5.3.0    5.6.1    5.6.1  gatsby-starter-casper
eslint-config-airbnb              17.0.0   17.1.0   17.1.0  gatsby-starter-casper
eslint-config-prettier             2.9.0   2.10.0    3.1.0  gatsby-starter-casper
eslint-plugin-import              2.13.0   2.14.0   2.14.0  gatsby-starter-casper
eslint-plugin-jsx-a11y             6.1.1    6.1.2    6.1.2  gatsby-starter-casper
eslint-plugin-react               7.10.0   7.11.1   7.11.1  gatsby-starter-casper
gatsby-link                       1.6.46   1.6.46    2.0.4  gatsby-starter-casper
gatsby-plugin-catch-links         1.0.24   1.0.26    2.0.4  gatsby-starter-casper
gatsby-plugin-feed                1.3.25   1.3.25    2.0.7  gatsby-starter-casper

こんな感じで色々出てくる

Wanted(必要最低限のバージョン) と Latest(最新版) を比較して
gatsby- prefix かつ、gatsbyjs/gatsbyのpluginをアップデートする

gatsbyのパッケージなら大体最新版でOKなはず
*ついでにgatsby無関係のパッケージも問答無用にLatestにしたけど、この環境じゃ大丈夫だった

$ npm i gatsby-plugin-xxx@latest gatsby-plugin-yyy@latest ...

latest(もしくはWanted以上任意の)にversion指定して npm i する

React と ReactDOM のインストール

v1 で内包されてた reactreact-dom の両パッケージが
v2 から peerDependencies になったので、
Gatsby プロジェクト側で明示的にインストールする必要がある

$ npm i react react-dom

Gatsby プラグインの peerDependencies のインストール

他パッケージに依存がある Gatsby プラグインのうち、 いくつかはpeerDependencies を持つようになっているので package.json を直接眺めるか cat package.json | grep gatsby-plugin- 見ながら 公式のプラグインライブラリで一つづつ検索して足りないものを追加していく

例えば、 gatsby-plugin-typography を使ってた場合は
こんな感じ

Install

npm install --save gatsby-plugin-typography react-typography typography

とあるので、プラグインの依存パッケージである react-typographytypography を入れる

$ npm i react-typography typography

破壊的変更の対応

Layout コンポーネントの削除 or リファクタ

v1 で各ページをwrapしていた Layout コンポーネント (src/layouts/index.jsx?) が
v2 ではサポートされなくなったので、そのまま動かすと大抵レイアウトがぶっ壊れる

次の移行手順がおすすめらしい

1. Layout コンポーネントの子 (children) の実行をやめる [必須]

v1 では関数 (render prop) として渡ってきていた children
React Components として渡ってくるため、() を取っ払い、そのまま評価する

src/components/layout.jsx
import React from 'react'

export default ({ children, location }) => (
  <div>
    <p>現在のpathはこれ: {location.pathname}</p>
-    {children()}
+    {children}
  </div>
)

2. layouts/index.jsxsrc/components/layout.jsx に移動 [必須ではないが推奨]

ただの component になるので、扱いも変える

$ git mv src/layouts/index.jsx src/components/layout.jsx

3. page/templateに Layout Component をimportして適用する

1で変更して2で動かした src/components/layout.jsx?
page と template で必要なところに自力で埋めていく

<Layout> で全体を単純にwrapすれば良い

src/components/layout.jsx
import React from 'react'
+ import Layout from '../components/layout'

export default () => (
+  <Layout>
    <div>Hello World</div>
+  </Layout>
)

Gatsby Casper Stearter v1.0.7src/pages だけ対応して src/templates はそのままでOK

4. history, location, match をpropで渡す

v1では <Layout> が直接 history, location, match に参照できていたが
v2では <Layout> は普通の component になるので pages だけが上記 3props に触れる

<Layout> 内部で、これらの参照がある場合、
<Layout> に依存するすべての pages から、必要なものをprop経由で明示的に渡す必要がある

src/copmponents/layout.jsx
import React from 'react'

export default ({ children, location }) => (
  <div>
    <p>現在のpathはこれ: {location.pathname}</p>
    {children}
  </div>
)
src/pages/index.jsx
import React from 'react'
import Layout from '../components/layout'

export default props => (
  <Layout location={ props.location }>
    <div>Hello World</div>
  </Layout>
)

5. queryStaticQuery を使うようにする

<Layout>data propを使ってmeta dataなど触っていた場合は <StaticQuery> を使って、普通のcomponentしての体裁を保つようにする

src/copmponents/layout.jsx
import React, { Fragment } from 'react'
import Helmet from 'react-helmet'
+ import { StaticQuery, graphql } from 'gatsby'

- export default ({ children, data }) => (
-   <>
-     <Helmet titleTemplate={`%s | ${data.site.siteMetadata.title}`} defaultTitle={data.site.siteMetadata.title} />
-     <div>
-       {children()}
-     </div>
-   </>
- )
-
- export const query = graphql`
+ const query = graphql`
  query LayoutQuery {
    site {
      siteMetadata {
        title
      }
    }
  }
`
+ export default ({ children }) => (
+   <StaticQuery
+     query={query}
+     render={data => (
+       <>
+         <Helmet titleTemplate={`%s | ${data.site.siteMetadata.title}`} defaultTitle={data.site.siteMetadata.title} />
+         <div>
+           {children}
+         </div>
+       </>
+     )}
+   />
+ )

公式のcode example だとqueryわざわざprop内部に書いてるけど、変数で渡していい

Layout に関してはこれで終わり

単純に置換しただけではダメで、import元を gatsby-link から gatsby に変更する必要がある

import React from 'react'
- import { navigateTo } from 'gatsby-link'
+ import { navigate } from 'gatsby'

// Don't use navigate with an onClick btw :-)
// Generally just use the `<Link>` component.
export default props => (
-  <div onClick={() => navigateTo(`/`)}>Click to go to home</div>
+  <div onClick={() => navigate(`/`)}>Click to go to home</div>
)

module周りの記述を CommonJS か ES6 どちらかに寄せる

webpack v1 から v4 になったので、 その間のBreakingChangesの一つでmodule systemが混在している場合にエラーが出るようになった

v2.2.0-rc.4-5

v2.2.0-rc.5 Breaking change:

  • In ES module (import or export exists in module):

    • exports is undefined
    • define is undefined
    • module.exports is read-only and undefined

v2.2.0-rc.4

  • Using CommonJS or AMD export stuff in a ES2015 module will emit errors.

この辺っぽい。具体例としては下記は公式からの引用。

All ES6 is 👍:

// GOOD: ES modules syntax works
import foo from "foo"
export default foo

All CommonJS is 👌:

// GOOD: CommonJS syntax works
const foo = require("foo")
module.exports = foo

Mixing requires and export is 🙀:

// BAD: Mixed ES and CommonJS module syntax will cause failures
const foo = require("foo")
export default foo

Mixing import and module.exports 🤪:

// BAD: Mixed ES and CommonJS module syntax will cause failures
import foo from "foo"
module.exports = foo

そもそも例にあるような import & module.exoprts, require() & export default のような
module systemが混在してるコードは見た人を恐怖させるので、避けたほうがよい

.babelrc を移動させる

v2ではBabel v7を使っているが、.babelrc がProject rootのものをlookupするような変更が入ったため、
Project固有の .babelrc を置くと意図せず gatsby のそれをOverrideしてしまう可能性がある

例えば、jest等に使ってる場合など。 方法としては2つあって

  1. .babelrcを捨てる

    • jestであれば jest.config.js 側に設定を逃がすなどして回避する
  2. Gatsbyのbabelrc と自分の設定をmergeする

PostCSS の plugin設定を追加する

v2では postcss-cssnextpostcss-import の暗黙的な設定が削除されたため これらを追加する必要がある

1. 依存パッケージのインストール

postcss-cssnextdeprecated になったため、
代わりに postcss-preset-env を入れる

$ npm install --save gatsby-plugin-postcss postcss-import postcss-preset-env postcss-browser-reporter postcss-reporter

上記の記事見る限り

Migration to postcss-preset-env

Migration should be pretty easy. In short, just replace postcss-cssnext with postcss-preset-env and choose your stage.

https://moox.io/blog/deprecating-cssnext/

とあるので基本置き換えるだけでOKで、必要ならstageを設定する

2. gatsby-plugin-postcssgatsby-config.js に追加する

gatsby-config.js
  plugins: [
+    'gatsby-plugin-postcss',
    'gatsby-plugin-xxxx',
    'gatsby-plugin-yyyy',
    //...
  ],

3. 各PostCSS pluginをincludeした postcss.config.js を置く

postcss.config.js
const postcssImport = require('postcss-import')
const postcssPresetEnv = require('postcss-preset-env')
const postcssBrowserReporter = require('postcss-browser-reporter')
const postcssReporter = require('postcss-reporter')

module.exports = () => ({
  plugins: [
    postcssImport(),
    postcssPresetEnv(),
    postcssBrowserReporter(),
    postcssReporter(),
  ],
})

ReactRouter から @reach/router への移行

React Router v4 から @reach/router にrouterライブラリが変更になったので、それに伴ってコードを修正する

APIは似てるのでそれほどぶっ壊れないとのことだけど、下記場合は修正する

@reach/routerでは、URL情報をobjectで渡すとよしなにparseするようなことはしないので自分で組み立てる必要がある

React Router v4
<Link
  to={{ pathname: `/about/`, search: `fun=true&pizza=false`, hash: `people` }}
>
  Our people
</Link>

これは

@reach/router
<Link
-  to={{ pathname: `/about/`, search: `fun=true&pizza=false`, hash: `people` }}
+  to={`/about/?fun=true&pizza=false#people`}
>
  Our people
</Link>

こうする 面倒なら URI.js とか使って組み立てる

state を渡すときは state prop を使う

今まで to Objectの中で渡してたであろう state
<Link state={}> として渡せるようになったのでそれを使う

const NewsFeed = () => (
  <div>
    <Link to="photos/123" state={{ fromFeed: true }} />
  </div>
)

const Photo = ({ location, photoId }) => {
  if (location.state.fromFeed) {
    return <FromFeedPhoto id={photoId} />
  } else {
    return <Photo id={photoId} />
  }
}

pageに渡ってた history propの代わりに @reach/routernavigate を使う

Navigationに使用するための history propが渡らなくなるので、 history操作したい場合は代わりに navigate 関数を使う

import { navigate } from "@reach/router"
  • exact
  • strict
  • location

@reach/router の仕様はデフォルトで exactstrict になる。 active/not activeのスタイリングに用いた location次項の getProps の使用 で対応する

active時などのスタイル適用は getProps を使う

<Link> について、Pathがcurrentな場合に activeClassName / activeStyle が適用される が、
もっと細かいことがやりたいなら getProps を使う

Argument obj Properties:

  • isCurrent - location.pathname と アンカーの href が等しいか
  • isPartiallyCurrent - location.pathname が アンカーの href と前方一致しているか
  • href - 相対パスやqueryStringなど諸々を解決したhref
  • location - アプリケーションの location
// location.pathname と完全一致していた場合のみ Active にする関数
// というかこれやるなら `activeClassName` 使ったほうが楽
const isActive = ({ isCurrent }) => {
  return isCurrent ? { className: "active" } : null
}

const ExactNavLink = props => (
  <Link getProps={isActive} {...props} />
)

// このリンクは location.pathname と完全一致しているか
// もしくは深いRoutingの場合に (=hrefと前方一致しているとき)
// Active になる
const isPartiallyActive = ({
  isPartiallyCurrent
}) => {
  return isPartiallyCurrent
    ? { className: "active" }
    : null
}

const PartialNavLink = props => (
  <Link getProps={isPartiallyActive} {...props} />
)

https://reach.tech/router/api/Link

これ見た感じ location も渡ってくるので、
コード変えたくない場合にも移行はそんなに難しくない

クライアントサイドルーティングに * を使用する

クライアントサイドルーティングを gatsby-node.js で行っていた場合は、
すべての子を指定する :path* に置換する

gatsby-node.js
exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions

  // page.matchPath is a special key that's used for matching pages
  // only on the client.
  if (page.path.match(/^\/app/)) {
-    page.matchPath = "/app/:path"
+    page.matchPath = "/app/*"

    // Update the page.
    createPage(page)
  }
}

その他 ReactRouter のクライアントルーティングの移行

クライアントルーティング周りで他に @reach/router 移行に必要なのは下記

  1. withRouter<Location> を使うように置き換える

    • <Location>{({ location, navigate }) => ... } のように直下の子に props.location が渡る
    • export時に withRouter するかわりに、こいつでWrapすればいい
  2. history object を触る代わりに import { navigate } from '@reach/router' を使う

  3. <Route> の廃止。 <Router> を使ってルーティングする。

下記は簡単な例。従来の React Router では

React Router
import { BrowserRouter as Router, Route } from 'react-router-dom'

export default () => (
  <Router>
    <div>
      <Route exact path="/" component={Home}>
      <Route path="/about" component={About}>
      <Route path="/store" component={Store}>
    </div>
  </Router>
)

このようにルーティングしていたものが @reach/router だと

@reach/router
import { Router } from '@reach/router'

export default () => (
  <Router>
    <Home path="/" />
    <About path="/about" />
    <Store path="/store" />
  </Router>
)

こうなる。APIが結構違う。 次は Gatsby Store<PrivatRoute> を React Router から @reach/router に移行したときの、すこし複雑な例。

 import React from 'react';
-import { Redirect, Route } from 'react-router-dom';
+import { Router, navigate } from '@reach/router';
 import { isAuthenticated } from '../../utils/auth';

-export default ({ component: Component, ...rest }) => (
-  <Route
-    {...rest}
-    render={props =>
-      !isAuthenticated() ? (
-        // If we’re not logged in, redirect to the home page.
-        <Redirect to={{ pathname: '/login' }} />
-      ) : (
-        <Component {...props} />
-      )
-    }
-  />
-);
+export default ({ component: Component, ...rest }) => {
+  if (!isAuthenticated() && window.location.pathname !== `/login`) {
+    // If we’re not logged in, redirect to the home page.
+    navigate(`/app/login`);
+    return null;
+  }
+
+  return (
+    <Router>
+      <Component {...rest} />
+    </Router>
+  );
+};

@reach/router 移行の具体例は下記リンクから見ることが出来る


続きは次の記事