React Hooksが出たのでFunctiona as ChildrenこじらせてたトイプロダクトをReact hooksとContextで書き換えた話
React 16.8で有効となったReact Hooks。魅力的な点は多々あるが、個人的に気になっていたのがState管理部分。
ReduxのStateの単一管理とはまた違っていて複数のState、Contextを使うような感じになってくる。
ただやはり単一Stateの管理はそれはそれで良い点を感じていて、「いやいやuseState本当にぐちゃぐちゃにならないのか?」という疑問があったりもした。
このあたりを検証したいなーと思っていたところ、以前Dart Sassをブラウザで動かしたときに作ったトイプロダクトがコードがStateを複数持たせたりして「うわーっ!爆発するー!」ってなってたことを思い出して書き換えをやってみたのでその雑感などをまとめたい。
今回の主題はトイプロダクトだからやったことなので、「さあ今すぐみんなHooksだ!」という趣旨のものではない(多分そういうのは推奨されてない)
書き換えた差分はこちら。(コミットメッセージが雑なのは・・・ほら・・・トイプロダクトだから・・・みたいなところで勘弁してほしい)
Contribute to terrierscript/dartystrap development by creating an account on GitHub.github.com
元のコードの問題点
今回対象としたプロダクトは「Dart Sassをブラウザ上で動かしてbootstrap4をコンパイルする」というものだった。
主題としてはDartの部分なので、表側のUIやらは「まあこのへんはReactでいいだろう。たいして複雑な状態管理もしない飽きたしReduxもいらんいらん!」という感じで進めていたと記憶している。
一方でバケツリレーもしんどいもんな〜というところがあったりで当時ハマってたFunction as Childrenを使いまくっていたのもカオスを増す結果を作っているなと読み返して感じた部分だった。
状態管理自体がそこまで複雑で無いとは言え、Sassを非同期にコンパイル処理してその結果を保持して・・・みたいな処理もあったりしてそこそこ複雑な処理になっていた。
まず要件としてはこんな具合を叶えたいものがこのへん
- Bootstrapのカスタムする変数部分をいじりたい
- いじった値をコンパイラに渡してコンパイルさせたい
- コンパイルは遅いので初回(componentDidMountのタイミング)だけコンパイルさせ、それ以降はボタンを押すことでコンパイルさせたい
- 再度コンパイルされる時に前回のworkerを止めたい
上記の要件を踏まえ、だいたいこんな具合になっていた
- 変数をVariableContextとしてコンテキストにする
- Submitterという中間コンポーネントを使い、VariableContextから値をもらってPropsに変換して初回だけCompilerに渡し、その後はビルドボタンを押した時のみCompilerへ渡すみたいなものを
- CompilerはPropsとしてPropsが変わったらコンパイルするシンプルなものにする
という形を作った。雑に簡略化するとこんな具合になる。
<VariableContext.Consumer>{ (variables) => {
<Submitter value={variables}>{(value, submit) => { // submitするとvariablesがvalueに反映されるみたいなやつ
<button onClick={() => submit()}>Generate</button>
<Compiler variables={value}>({(css) => {
...
コンパイラの代わりにフィボナッチ関数で簡略化したのも置いておく(これを書いてる時にinitialValueみたいなものをCompilerに持たせればSubmitterなんて複雑なものいらないじゃんということに気付いて修行不足を感じたがこの件は割愛する)
CodeSandbox is an online editor tailored for web applications.codesandbox.io
本題:React Hooksにするとどうなったか?
やはりuseEffectが非常に便利
上記のコンパイル部分についてだが、Class ComponentではcontextをcomponentDidMountで呼び出すことは推奨されてないことも複雑さの一端を担っていたと言える。
これがuseEffectによってかなりすっきり書けたると感じた
正直なことを言うと、「ホントに?ホントにuseContextとuseEffect組み合わせて大丈夫・・・?」みたいな心配はちょっとだけあるけどrenderに相当するタイミングでhooksに入るはずなので多分大丈夫だと思う・・・
細かくStateを分離するのは良さそう
Reduxを使わないとはいえ、これまでのReact ComponentではClass Componentを使いたくないなどがあって不必要にstateが肥大化して、同時にそれに絡むハンドラーなんかも太くなる傾向があった。これはそれぞれのstateとハンドラーを useXxxx とまとめることでいくらか可読性を担保できると感じた。
例えば今回「コンパイラをworkerで動かすかどうか」というチェックボックスの状態を分離して、これはこれでいいかもなと思った。
不用意なHoCsやFunction as Childrenがなくなる
「コンポーネントだけで構成される」というReactの世界においてFunction as ChildrenやHoCsの発明は確かに有益なものだったが、HoCsの型周りの面倒さやどうしても可読性が下がる点があったり、Function as Childrenはコンポーネントなのに主体が関数であるみたいな読み応えとして不可解になるケースも少なくなかった。
Hooksが入ることで実は一番縁遠くなるのはこの辺になるだろうと感じた。もし最初にHooksを導入するなら「なんだかこのへんシッチャカメッチャカなんだよな」というHoCsやFunction as Childrenを標的にしてみるといいかもしれない
コンポーネントは小さく。SmartなコンポーネントとPresentationalなコンポーネントを分けるという原則は相変わらず有益
Hooksが入っても変わらないだろうなと思ったことがこれだ。やはり書き換えてる最中にコンポーネントが小さければ小さいほど書き換えやすいなと感じたし、stateなど状態を気にするコンポーネントとそうでないコンポーネントがきっちり別れていない部分ほど手こずるなと感じた。
hooksが入ってしまうことで行数的には縮んでしまったり逆にstateなどライトに使えてしまう側面があるが、ここは気を引き締めてとにかく分離してくように心がけたほうが良いなと感じた
Contextの扱いは考えものかも?
Stateをバラけさせても結局共通化させなければならず、その際に出てくるのがcontextになる。
contextは救世主的に便利なのだが、Providerの指定漏れが起きることがあり悩ましいなーと感じた。Providerの設定を忘れるとcreateContext(defaultValue)で設定した値が来てしまうので結構気づかないことがある。contextがライブラリ向けのものという側面もあるので、そもそもProviderが無くて動くように出来てるのはそりゃーそうだよねという感じなのだが、stateと組み合わせるケースだと悩ましい。
対策としてReduxが内部で行ってるようにcreateContext(null)のような事をするという手もあるのだが、TypeScriptだとこれはこれで面倒というのがあってこれもこれで悩ましい。。。
useContextが複数絡み合うと「あ、これはやばいかもな」という気配は持ってしまうのでこの辺は少し考えないとだめかもしれない。
ひとまずの暫定対策としては、下記のようになるだろうか
- ちょっと型付けが面倒でもdefaultValueはnullにしておく
- 複数のProviderはアプリケーションの上部でまとめて行う
- 絶対にProvider漏れを起こしたくない場合はFunction as Childrenの方を利用する
- 初期化されてる事を確認するsafetyContextみたいなものを自作する(考えてはみたけど、いやーこれも微妙だ・・・)
- constateの内部実装を見るとProxyを使って呼び出されたらエラーを吐くようにしていてなるほど〜という感じがある
いずれにしてもちょっと微妙だ。もう少しこの辺は様子見してもいいかもしれない
あと、そもそも自分がContextをちゃんと理解してなかったなーというのは色々いじってみて思った部分だった。結構ここは素振りしたほうが良い箇所かもしれない
useStateにfunctionを入れる時に気をつけよう
react-hooksの`useState`はこれまでのstateのように値を保持してくれる重要な関数だ。 例えば単純なカウンターならこんな具合になるだろう ```jsx const useCounter = () => { ...qiita.com
こっちに書いたが結構ハマった。ちなみに今回のコードだとworkerのterminateを保持する場所でやってしまっていて「なーんでこれ止まってるんだ?」としばらく悩んでいた。
結果refを使ってstateに戻すことも出来たかがそのままにしている。実はあんまりこれまでrefsを使う事案に出会って無くてrefsをちゃんとわかってないので正しい使い方なのか微妙かもしれない
reduxのmiddlewareはどうなるだろう?という想像
今の所のstateを見るに、まだreduxを捨てるかどうかは微妙なとこかもなーと思いつつ、 middlewareはもうhooksに置き換えるだろうなーとは思った。個人的にはredux-observableが大好きで使っていたが、おそらく今後は出番が減るだろうなーとは感じている。
今までコンポーネントはActionを飛ばして、それをゴニョゴニョobservableでやるという部分が、componentで必要なときだけrxjs-hooksなどでRxを使って処理するとかになっていくだろうなと感じる。ajaxの部分などはまた別にhooksが使えると思うので、Rx部分はよりそれ専門の部分だけになっていくだろうと感じる(でも、Rxの書き方や考え方は何も変わらないので、この点はやっぱり素敵を感じる)
createContextのimportに気をつけよう
よく見もせずにVSCodeのimportをしてると30分失うので気をつけたい