React Hooksのライブラリ開発(web向け)をしたい人のためのテンプレート
TL; DR
React HooksのCustom Hooksをnpmのライブラリとして切り出して開発したい人用のGitHub Repository Templateです.スターもらえるとめっちゃ喜びます.
対応してるもの
- TypeScript (
.ts,.tsx) - React v17+ (jsxの変換の都合上)
- Jestでのunit test
- その他諸々 (下記参照)
概要
Reactを触ったことのある方なら,Hooksはご存知だと思います.
一応めちゃくちゃざっくりとした解説をいれておくと,useXXX みたいなやつです.知らない方は1度触ってください.これはお願いです.
そして,useXXX みたいなのは自分でも定義できます.これをCustom Hooksといいます.
Custom Hooksでいい感じにまとめられるようになると,コードの見通しが一気にすごくよくなるので積極的に使いましょう.
そして,この “いい感じにまとめる” のがうまくなってくると,もはやプロダクトを越えて共有したくなってきます.そういう例はこのへんにたくさんあります.
しかし,npmへのライブラリの公開というのは,プロダクトをつくることとはまた別次元の難しさがあります. (実は今回のものはCDまでは対応していません.Kitをnpmに公開したくないため,それに関してはまた違うところでやるためです.構想は下のほうにあるので,そのへんは各々でやってみてください.気が向いたら私もテンプレートとしてコードを追加するかもしれません.)
実は,私はいくつかこうしたKitをprivateで持っています.今回その1つの hookskit を公開したのは,npmのライブラリ作成に関する公開されている知見が本当に少ないと感じたためです.
本記事では,その有象無象 (私の甘い理解含む) と紆余曲折を記すことで,なにか少しでも役に立てば,と思い書くことにしました.
なお,GitHubでTemplateとして公開しているのは,単純にCRAのようなものがあまり好きではないというだけのことです.
説明
ビルドツール
最初これを作り始めたときはtscにすべて頼っていました.しかしtscはbuild後の成果物のpathsとbaseUrlを解決してくれないので,私の好む
import { useExample } from '/example'
みたいな / で始まるスタイルをうまく解決することができませんでした.
いろいろと見て回ったところ,tscを最後のbuildに使う世の中のライブラリの大体が
- tscを使う場合,相対パスでのimportでやりくりしている
- 絶対パスでのimportにする場合,tscを最終buildに使っていない
のどちらかのパターンでした.
また,tscでbuildする場合,ESMやCJSスタイルでの書き出しをするのが一度にできないため結局いろいろと面倒な点があり,違う方法を探すことにしました.
そこで,次はesbuildを使うことにしたのですが,esbuildは型情報を落としてしまうだけなのでd.tsを生成するには結局tscを使う必要があります.すると結局ビルド速度が速い利点が霞んでしまうのと,ビルドスクリプトもTypeScriptで書きたいのでそうするとts-nodeを使ってビルドスクリプトを走らせる必要がありますが,そうすると (なんらかの理由で) tsconfigを3つか4つ持っておく必要が出てきてしまい,これでは面倒だなということでesbuildを外しました.
途中でtsdxも使いましたが,ちょっとほしいものと違ったのでやめました.
結果,microbundleを使いました.このツールについては日本語で唯一?の文献がこれです.
元々preactのためのbuildツールで,package.jsonの中の設定をみて書き出してくれます.import pathのresolveなども勝手にやってくれるのでいいのですが,現状
- 複数のinputファイル → 複数のoutputファイルができない
-
d.tsの書き出しが完全にtsc依存
であるので,必ずindex.tsのようなものを持っておく必要があります.
そしてmicrobundleの注意点として,d.tsに関してはtscに完全依存であるためpackage.jsonのtypesにindex.d.tsと書いていても,sourceがexport.tsのような場合,吐き出されるのはexport.d.tsです.ここが結構ハマりポイントでした.
またtscにはconcatenate機能もないため (moduleオプションがamdかsystemのときはoutFileが使えるが今回はだめ),要するにsrcの中のtsファイルそれぞれに対応する形でd.tsが吐き出され,d.ts内のimport pathがresolveされず,importする側で期待する挙動をしません.
この問題は結局の所webpackやrollupを使ってpluginでどうにかするか,ttypescriptを使うしかないのですが,microbundleの作者はttypescriptの導入には反対のようで (わかる),こうなるともう後付けでやるか,webpackやrollupのconfigを書くしかありません(やりたくない).
ここで使えるのがtsc-aliasです.
これはtscの成果物に対してimport pathをtsconfigを見て解決してくれます.はっきり言って神です.この作者のGitHub Sponsorがオンだったら速攻でSponsoringしてました.
これで晴れてビルドできるようになりました.
テスト
私はGoを書くときはTDDが大好きですが,フロントのHooks以外の開発ではTDDはあまり使いません.理由はUIまわりのテストは労多くして功少なしな気がしてしまうからなのですが (なのでやるとしたらVRTとかでしょうか),この辺はまたの機会に.
一方,HooksはUIと疎結合にしてあればテストしやすいはずです.ただし,ものによってはpreconditionを用意しにくいため,このへんはケースバイケースでしょう.
というわけで,unit testは書いていきたいのでJestを使うことにしました.srcディレクトリに*.test.{ts,tsx}の形で書いていきます.
ここで若干面倒だったことが2点あり,
-
d.tsの生成にtscを使っているため,build時にtestの型情報が生成されないようにするにはtsconfigのexcludeにtestを除外するよう指定する必要がある.しかしそうすると開発時にtestをかけないため,build用のtsconfigと分ける必要がある.(私はすでにオプションが多すぎるので分けていたので問題なし) -
microbundleにはpackage.jsonに"type": "module"を記述するようにとある(ESMをデフォルトにするオプション)が,これをするとts-jestが動かない (https://github.com/facebook/jest/issues/9430 このへんに解決策の提案は色々あるが,現状自分の環境ではどれも動かなかった).package.jsonの"type": "module"を外してもとりあえずmicrobundleの動作には問題がなさそうなので,外すことで対応
というわけで,unit testは書けるようになりました.
ただし私の経験上,npmのライブラリ開発は大抵importしてみないと見えないエラーも多々ある (実はうまくimportできない,d.tsが適切ではないなどなど) ので,絶対にimportしてみるテストは書いたほうがいいです.
最低限ということで,今回はexamplesディレクトリにbasicというごく単純な例をおき実験しています.本来ならbuild可能を確かめるだけでなく,こっち側でもJestを使い,最低限renderできるかはテストすべきだが今回は省いてます.
なお,このexampleをtestに使うのはGoの発想です.
今回はどちらかというとintegration testっぽいけれども.
この “importしてみる” には,yalcを使ってます.yarn linkだとエラーが出るようになってしまったのと,より実際の状況に近づけるためです.
これはとくに引っかかる点なくできると思うのでとくに解説はありませんが,敷いて言えば,今はまだpublishingしていないためyalcをgitに含めてます.publishing後はできればexampleの中のdependencyをnpmの方に向けて,yalc linkでGitにはyalcを含めなくていいようにしたいです (とはいえyalcは必要なのでpackage.jsonのdependencyには残るはず).
継続性
最後に継続性の話です.
私のKit全般に言えることなのですが,必ずRenovatebotを入れるようにしています.これはdependencyを最新にするPRを自動的に作成するサービスで,templateに限らず入れておいたほうがいい物ですが,特にtemplate repoに入れておくことでUse Templateしたときにいつでも最新である安心感があります.
さらにdependencyを最新にしたときに落ちないことを確認するため,GitHub Actionsでテストをかけています.これはもちろんRenovatebotのPRに対しても有向なため,今後Reactがアップデートしたときも安心です.
GitHub Templateの残念なところとして,Repoを作ったあとTemplateに入ったアップデートは自動的に反映されません (そりゃそう).
また,Templateも気をつけていないとすぐに廃れてしまいます (よくあるのが手元の開発中のものだけでeslintの設定を変えて,templateには反映しないとか).
今回はこれに対する解決策ではないですが,なるべくTemplateを継続したいので,せめてRenovatebotを利用しているところです.
いずれ,templateとの差分を出す機構とかを作っていきたい所存です.
あとは,JavaScriptがGitHubの

ここに出るのが嫌だったので,eslintのconfigやprettierのconfigは全部yamlで書いてます.
CD
今回は書いてないですが,CDに関して,
- npmのtokenをGitHubのSecretsに書く
- ローカルの手作業でpackage.jsonのversionをあげる.
- リリースすると決まったら,tagを打つ (
v1.0.0とか) - tagのpushがあったら,GitHub Actionsで
- tagのバージョンとpackage.jsonのバージョンが一致するか確認
-
can-npm-publishでpublishできるかチェック - OKだったらpublish
というフローが1番いいと思ってます.
昔はmasterにmergeされたら〜とかがいいと思ってたんですが,あっ…無理…ってなったのが,
これです.できればpackage.jsonのバージョンを外部から埋め込む,とかすればいいんですけどねぇ… 開発するときに支障が出そうでやってないです.
わからないこととか
- なんとなくRenovatebotでいつもやるようにPin Dependencyしてるんですが,ライブラリ用途だとこれはやめておくべきな気がしてます (その場合はRenovatebotはどう更新するんだろう,lockだけの更新になるのかしら…) → 書いてありました https://docs.renovatebot.com/dependency-pinning/ とはいえfirebaseのsdkとかはpinしてるんですよね.
- 次にやりたいことに,ReactのUIライブラリの作成のためのtemplate作成があります.そこではjsxの変換をemotionを使う予定なのですが (
csspropsを使いたい),emotionをdependencyとするべきなのか,peerDependencyとするべきなのか…. - Tree Shakingがあるから1ファイルにbundleしていいのか,それともファイルを分割すると利点があるのか (例:
@material-ui/core/Button的な) - どこまでbabelかけるべきなのか.IEとかで動かしたい用途の場合はプロジェクトもとでnode_modules向けにbabelをかけるべきだとは思うが,どうサポートすればいいのかが難しい.この辺あまり深く考えたことがないのでちゃんと調べたい
Discussion