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

2019-03-23 updated

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) の実行をやめる [必須]
      2. layouts/index.jsxsrc/components/layout.jsx に移動 [必須ではないが推奨]
      3. page/templateに Layout Component をimportして適用する
      4. history, location, match をpropで渡す
      5. queryStaticQuery を使うようにする
    • navigateTonavigate に変更する
    • module周りの記述を CommonJS か ES6 どちらかに寄せる
    • .babelrc を移動させる
    • PostCSS の plugin設定を追加する

      1. 依存パッケージのインストール
      2. gatsby-plugin-postcssgatsby-config.js に追加する
      3. 各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 のクライアントルーティングの移行
    • onPreRouteUpdateonRouteUpdateaction を引数に取らなくなった

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

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
<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
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 移行の具体例は下記リンクから見ることが出来る

onPreRouteUpdateonRouteUpdateaction を引数に取らなくなった

React Router v4 はルートの遷移が行われるとき、action (push/replace) を引数の一つとして location と共に渡っていたが、@reach/router はこれをサポートをしない。

具体的には v1の onRouteUpdate

/*
 * destructured object
 *
 * location {object} A location object
 * action {object} The `action` that caused the route change
 */
exports.onRouteUpdate = ({ location, action }) => {
 //...
}

このように action を持っているが v2の onRouteUpdate

/*
 * destructured object
 *
 * location {object} A location object
 * prevLocation {object|null} The previous location object
 */
exports.onRouteUpdate = ({ location, prevLocation }) => {
  console.log('new pathname', location.pathname)
  console.log('old pathname', prevLocation ? prevLocation.pathname : null)

  // Track pageview with google analytics
  window.ga(
    `set`,
    `page`,
    location.pathname + location.search + location.hash,
  )
  window.ga(`send`, `pageview`)
}

前の location オブジェクトが渡るように変更されている。
このため action に依存した処理がある場合は、それを削除するか修正する必要がある。 (v1のDocざっと見た感じ onPreRouteUpdate 生えてなさそうだけどどういうこった・・・?)

ブラウザ API replaceRouterComponent が削除

React Routerhistory を差し替えることができたので、 GatsbyreplaceRouterComponent を使ってカスタムした history または React Router を使うことが可能だった。@reach/router はこの機能がないので Gatsby の API から削除された。

もし、この API を Redux のようなライブラリ・フレームワークをサポートするため、root コンポーネントをラップする用途で (間違って) 使ってたときは、 wrapRootElement として移行する必要がある。

import React from 'react'
import { Provider } from 'react-redux'
-import { Router } from 'react-router-dom'

-export const replaceRouterComponent = ({ history }) => {
+export const wrapRootElement = ({ element }) => {
-  const ConnectedRouterWrapper = ({ children }) => (
+  const ConnectedRootElement = (
    <Provider store={store}>
-      <Router history={history}>{children}</Router>
+      {element}
    </Provider>
  )

-  return ConnectedRouterWrapper
+  return ConnectedRootElement
}

ブラウザ API replaceHistory が削除

ブラウザ API replaceRouterComponent が削除 と同じ理由で廃止になった。もし、replaceHistory()history.listen() を使って、ページ遷移を listen するような処理がある場合は、onRouteUpdate を使うように書き換える。

ブラウザ API wrapRootComponentwrapRootElement に置き換え

新しい API wrapRootElement は、 Root コンポーネントではなく component 要素(※紛らわしい)を渡すようになったので適宜置き換える。

-export const wrapRootComponent = ({ Root }) => {
+export const wrapRootElement = ({ element }) => {
-  const ConnectedRootComponent = () => (
+  const ConnectedRootElement = (
    <Provider store={store}>
-      <Root />
+      {element}
    </Provider>
  )
-  return ConnectedRootComponent
+  return ConnectedRootElement
}

続きは随時追加します

  • 2018-10-07 posted
  • 2019-03-23 updated