npmパッケージを開発中、デモページも同じリポジトリで開発してたら思わぬところでハマったので経緯と解決法を記録しておく。
経緯
ReactでシンプルなJSONエディタがほしくて作って
Reactでシンプル(by Plain Text)なJSON Editorを作ったデモ。
— 岩谷成晃 (@nariakiiwatani) August 10, 2020
入力された文字列をそのページ自体のCSS Propertiesとして使用している。https://t.co/lLXlXGmCzOhttps://t.co/pYLba2zmTS#react #react-hooks #jsoneditor #json #npm pic.twitter.com/mOIawqyiTi
次回別のコンポーネントを作って公開する際にやることを最小化できるように、boilerplateとして切り出してまとめて
git pushでnpmパッケージとデモページを更新する(React&TypeScript用のboilerplate付き) https://t.co/mJrzV9ssxA #Qiita
— 岩谷成晃 (@nariakiiwatani) August 11, 2020
せっかくなのでreact-plain-json-editorもこのboilerplateにしたがって編集しなおしたところ、デモページが動かなくなったので
live demoが死んでる
— 岩谷成晃 (@nariakiiwatani) August 11, 2020
修正した!
修正した!
— 岩谷成晃 (@nariakiiwatani) August 12, 2020
現象
デモページを開くとブラウザで下記のエラーが出現した。
(非常に丁寧なエラーメッセージですね)
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
at resolveDispatcher (react.development.js:1465)
at Object.useState (react.development.js:1496)
at ../dist/PlainJsonEditor.js.exports.PlainJsonEditor (PlainJsonEditor.js:38)
at renderWithHooks (react-dom.development.js:14803)
at mountIndeterminateComponent (react-dom.development.js:17482)
at beginWork (react-dom.development.js:18596)
at HTMLUnknownElement.callCallback (react-dom.development.js:188)
at Object.invokeGuardedCallbackDev (react-dom.development.js:237)
at invokeGuardedCallback (react-dom.development.js:292)
at beginWork$1 (react-dom.development.js:23203)
よくある原因として挙げてくれている3点のうち、1,2は大丈夫そうだったので、3が怪しい。
3. You might have more than one copy of React in the same app
(意訳)Reactが複数含まれているかもしれません
ざっくり現象の概要をまとめてしまうと、「__ライブラリ本体とデモページとで別々のReactを参照している__ので不整合が起きている」ということになるかと思う。
とりあえず結論だけ知りたい方はこちらへ。
ちなみに、現象としてはこちらのページに書かれているものとおそらく同じだが、少し違う対処をした部分もあるのでこちらはこちらで記しておく。
Reactコンポーネントをnpmパッケージとして開発する
状況の整理
- ライブラリ本体の開発
- 開発フォルダをルートディレクトリ(
/
)と呼ぶことにする - ビルドツールは
tsc
- ビルドに関連するファイルは
package.json
- 開発フォルダをルートディレクトリ(
- デモページの開発
- 開発フォルダは
demo
ディレクトリ - ビルドツールは
webpack
- ビルドに関連するファイルは
demo/package.json
とdemo/webpack.config.js
- 開発フォルダは
問題と対処
問題1: ライブラリ本体のdependenciesにreactが入っている
これはそもそもよくない。普通は
- 動作に必要なパッケージ =>
dependencies
- 開発に必要なパッケージ =>
devDependencies
だが、パッケージとして公開することが前提であれば、使用側との競合を避けるため
- 動作に必要なパッケージ =>
peerDependencies
- 開発に必要なパッケージ =>
devDependencies
とするべきだ。
ただし、これだけではpeerDependencies
はyarn install
(またはnpm install
)に無視されるので、開発時にビルドできなくなる。
問題1への対処
- 動作に必要なパッケージ =>
peerDependencies
とdevDependencies
の両方 - 開発に必要なパッケージ =>
devDependencies
とするのが良さそうだ。
問題2: node_modules
とdemo/node_modules
の両方にreact
が含まれている
問題1に対処しても、まだreact
の競合は避けられていないので、
どちらかだけに含むようにする(またはどちらかだけを参照するようにする)必要がある。
修正前
"dependencies": {
"react": "^16.13.1"
}
// ルートディレクトリを直接参照
import { PlainJsonEditor } from '../../../'
// 実際にはこれは書いていなかったが、説明のためにデフォルト値で記載。
resolve: {
modules: ['node_modules']
}
これらの指定により、デモページ側ではdemo/node_modules
にあるreact
が参照されてしまう。
修正方針
実際にパッケージが使用される環境を想定すると、react
はライブラリ側ではなくdemo側にインストールされる方が正しくはあるが、開発環境が不便になるので、ライブラリ側だけにインストールし、demo側からルートのnode_modules
を参照できるようにする。
問題2への対処
yarn link
またはnpm link
を使っておいてから"react-plain-json-editor": "latest"
などを指定しても良いが、誰か別の人が使うときに管理する状態を極力増やさないようにしたいので今回は"link:../"
を使った。
"dependencies": {
"react-plain-json-editor": "link:../"
}
// dependenciesでlinkしたので、通常のパッケージをimportする時と同じ記法で使用可能
import { PlainJsonEditor } from 'react-plain-json-editor'
resolve: {
// ルート側を優先して参照するようにする
modules: ['../node_modules', 'node_modules']
}
ちなみにこのdemo/webpack.config.json
の修正後も、demo/node_modules
にreact
が含まれていると同様のエラーが起きた。
ここはwebpackの仕様の話になるのだろうが、これ以上は調べていない。
問題3: package.json
にバージョン指定がない
※今回の問題とは直接の関係はない
パッケージのリリースはGitHub Actionsに登録したsemantic-release
に任せており、バージョン番号はsemantic-release
が管理するので、package.json
にバージョンを書くと冗長だったり公開バージョンとの間に見た目上の不整合が生じたりすると思って、version
の指定自体を削除していた。
するとdemoのyarn install
時に下記エラーが発生した。
error Can't add "react-plain-json-editor": invalid package version "".
問題3への対処
package.json
に"version": "0.0.0"
を追加
以上で問題なくデモページが動くようになった。
成果物はこちらです。
react-plain-json-editor
dev-npm-package-react-typescript-boilerplate
まとめ
react
(及びライブラリ本体が他に依存するパッケージ)はpackege.json
のpeerDependencies
とdevDependencies
の両方に記載し、dependencies
には記載しない。
-
peerDependencies
: このパッケージをinstallする際に必要に応じてWarningを出す -
devDependencies
: 開発時のためにnode_modules
にインストールされる
demo/node_modules
にreact
を含めない
-
demo/package.json
のdependencies
にreact
を書かない
デモページのビルド時にルートのnode_modules
を参照するようにする
-
demo/webpack.config.js
のresolve.modules
に['../node_modules', 'node_modules']
を指定する-
../node_modules
: ルートディレクトリ側のnode_modules
のこと -
node_modules
: デモページ側のdemo/node_modules
のこと
-
デモページから開発中のライブラリを使用できるようにする
-
demo/package.json
のdependencies
に"react-plain-json-editor": "link:../"
を記載 - または
yarn link
を設定後"react-plain-json-editor": "latest"
を記載
# まずはルートディレクトリで
yarn link # このパッケージをlinkコマンドで参照可能にする(`~/.config/yarn/link`にシンボリックリンクが貼られる)
cd demo # demoへ移動
yarn link react-plain-json-editor # linkで登録したパッケージをこのプロジェクト(demo)で使えるようにする
参考にしたページ
Reactコンポーネントをnpmパッケージとして開発する
Yarnでローカルのパッケージをaddする方法
npm linkの基本的な使い方まとめ
0000-link-dependency-type.md
依存関係の種類
Webpackを使う時は、resolve.modulesの設定に気を付けよう。(特にModule not found: Error: Can't resolve~が表示された時)
Resolve