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 リファクタ
- Layout コンポーネントの子 (
children
) の実行をやめる [必須] layouts/index.jsx
をsrc/components/layout.jsx
に移動 [必須ではないが推奨]- page/templateに
Layout Component
をimportして適用する history
,location
,match
をpropで渡すquery
をStaticQuery
を使うようにする
- Layout コンポーネントの子 (
navigateTo
をnavigate
に変更する- module周りの記述を CommonJS か ES6 どちらかに寄せる
.babelrc
を移動させる-
PostCSS の plugin設定を追加する
- 依存パッケージのインストール
gatsby-plugin-postcss
をgatsby-config.js
に追加する- 各PostCSS pluginをincludeした
postcss.config.js
を置く
- ReactRouter から @reach/router への移行
<Link>
のto
propをString
にするstate
を渡すときはstate
prop を使う- pageに渡ってた
history
propの代わりに@reach/router
のnavigate
を使う <Link>
の廃止になったpropsの対応- active時などのスタイル適用は
getProps
を使う - クライアントサイドルーティングに
*
を使用する - その他 ReactRouter のクライアントルーティングの移行
onPreRouteUpdate
とonRouteUpdate
がaction
を引数に取らなくなった
-
依存パッケージのアップデート
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 で内包されてた react
と react-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-typography
と typography
を入れる
$ npm i react-typography typography
破壊的変更の対応
Layout コンポーネントの削除 or リファクタ
v1 で各ページをwrapしていた Layout コンポーネント (src/layouts/index.jsx?
) が
v2 ではサポートされなくなったので、そのまま動かすと大抵レイアウトがぶっ壊れる
次の移行手順がおすすめらしい
1. Layout コンポーネントの子 (children
) の実行をやめる [必須]
v1 では関数 (render prop) として渡ってきていた children
が
React Components
として渡ってくるため、()
を取っ払い、そのまま評価する
import React from 'react'
export default ({ children, location }) => (
<div>
<p>現在のpathはこれ: {location.pathname}</p>
- {children()}
+ {children}
</div>
)
2. layouts/index.jsx
を src/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すれば良い
import React from 'react'
+ import Layout from '../components/layout'
export default () => (
+ <Layout>
<div>Hello World</div>
+ </Layout>
)
Gatsby Casper Stearter v1.0.7 は
src/pages
だけ対応して src/templates
はそのままでOK
4. history
, location
, match
をpropで渡す
v1では <Layout>
が直接 history
, location
, match
に参照できていたが
v2では <Layout>
は普通の component
になるので pages
だけが上記 3props に触れる
<Layout>
内部で、これらの参照がある場合、
<Layout>
に依存するすべての pages
から、必要なものをprop経由で明示的に渡す必要がある
import React from 'react'
export default ({ children, location }) => (
<div>
<p>現在のpathはこれ: {location.pathname}</p>
{children}
</div>
)
import React from 'react'
import Layout from '../components/layout'
export default props => (
<Layout location={ props.location }>
<div>Hello World</div>
</Layout>
)
5. query
を StaticQuery
を使うようにする
<Layout>
で data
propを使ってmeta dataなど触っていた場合は
<StaticQuery>
を使って、普通のcomponentしての体裁を保つようにする
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
に関してはこれで終わり
navigateTo
を navigate
に変更する
単純に置換しただけではダメで、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.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つあって
-
.babelrcを捨てる
- jestであれば
jest.config.js
側に設定を逃がすなどして回避する
- jestであれば
- Gatsbyのbabelrc と自分の設定をmergeする
PostCSS の plugin設定を追加する
v2では postcss-cssnext
と postcss-import
の暗黙的な設定が削除されたため
これらを追加する必要がある
1. 依存パッケージのインストール
postcss-cssnext
が deprecated になったため、
代わりに 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.
とあるので基本置き換えるだけでOKで、必要ならstageを設定する
2. gatsby-plugin-postcss
を gatsby-config.js
に追加する
plugins: [
+ 'gatsby-plugin-postcss',
'gatsby-plugin-xxxx',
'gatsby-plugin-yyyy',
//...
],
3. 各PostCSS pluginをincludeした 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は似てるのでそれほどぶっ壊れないとのことだけど、下記場合は修正する
<Link>
の to
propを String
にする
@reach/routerでは、URL情報をobjectで渡すとよしなにparseするようなことはしないので自分で組み立てる必要がある
<Link
to={{ pathname: `/about/`, search: `fun=true&pizza=false`, hash: `people` }}
>
Our people
</Link>
これは
<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/router
の navigate
を使う
Navigationに使用するための history
propが渡らなくなるので、
history操作したい場合は代わりに navigate
関数を使う
import { navigate } from "@reach/router"
<Link>
の廃止になったpropsの対応
exact
strict
location
@reach/router
の仕様はデフォルトで exact
と strict
になる。
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など諸々を解決したhreflocation
- アプリケーションの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} />
)
これ見た感じ location
も渡ってくるので、
コード変えたくない場合にも移行はそんなに難しくない
クライアントサイドルーティングに *
を使用する
クライアントサイドルーティングを gatsby-node.js
で行っていた場合は、
すべての子を指定する :path
を *
に置換する
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
移行に必要なのは下記
-
withRouter
を<Location>
を使うように置き換える<Location>{({ location, navigate }) => ... }
のように直下の子にprops.location
が渡る- export時に
withRouter
するかわりに、こいつでWrapすればいい
-
history object を触る代わりに
import { navigate } from '@reach/router'
を使う- pageに渡ってた
history
propの代わりに@reach/router
のnavigate
を使う と同じ - この
navigate
は<Location>
が子に渡すprops.navigate
と同じもの
- pageに渡ってた
<Route>
の廃止。<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
だと
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 移行の具体例は下記リンクから見ることが出来る
onPreRouteUpdate
と onRouteUpdate
が action
を引数に取らなくなった
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 Router
は history
を差し替えることができたので、 Gatsby
は replaceRouterComponent
を使ってカスタムした 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 wrapRootComponent
が wrapRootElement
に置き換え
新しい 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