HOC と RenderProp と Hook の相互変換はどこまで可能か
React で Cross-Cutting-Concern の文脈で語られ比較されるものと言えば HOC と RenderProp と Hook ですね。
実際に開発を行っているとライブラリから提供される機能がそのどれかのみで、プロジェクトとしては別の形で用いたいのでそれらを相互変換する、ということがたまにあります。
- Hook は便利だけどクラスコンポーネントでは使いづらいので HOC や RenderProp 版が欲しい
- Hook は便利だけどコンディショナルな箇所や map する箇所ではそのまま使えないので RenderProp が欲しい時もある
- HOC だと props の型定義が面倒なので RenderProp でサクッと実装したい
- HOC だと型パラメータが潰されるので RenderProp で回避したい
- RenderProp だとネストが増えるので HOC で注入したい
...といった需要によるものです。 ただ、実際にやってみると、どうやら変換できなさそうな組み合わせがあることに徐々に気づいてきたので、今回はその感覚を整理するために、シンプルな例を題材にして、相互変換の可能性や変換時のポイントなどを試行錯誤してみたいと思います。
実際に動作するコードは github においておきました。
共通する型定義
まずは型定義です。foo
という何かを提供する MyFeature
という機能をコンポーネントから利用可能にするライブラリを考えることにします。
export interface MyFeature { foo: string } export const myFeature: MyFeature = { foo: 'Yes!' } // For RenderProp ver. export interface MyFeatureProps { children: (feature: MyFeature) => JSX.Element } // For HOC ver. export type WithMyFeature = { feature: MyFeature }
各パターンを組んでみる
HOC から変換する
ライブラリから HOC として機能が提供される場合、それを RenderProp や Hook として変換できないか試してみます。
import React, { createElement } from 'react' import { myFeature, WithMyFeature, MyFeatureProps } from './my-feature' // ライブラリから HOC として機能が提供 export function withFeatureFromHoc<P extends {}>(C: React.ComponentType<P & WithMyFeature>) { return (props: P) => <C feature={myFeature} {...props} /> } // HOC → RenderProp (*˘꒳˘*) ちょっときもちわるい... export function FeatureFromHoc(props: MyFeatureProps) { return createElement(withFeatureFromHoc(({ feature }) => { return props.children(feature) }), props) } // HOC → Hook (*˘꒳˘*) かけない... export function useFeatureFromHoc() { return null }
残念ながら Hook はうまく組めませんでした。
HOC → RenderProp も動きはするのですが、コードがちょっと不安になりますね。
RenderProp から変換する
ライブラリから RenderProp として機能が提供される場合、それをHOC や Hook として変換できないか試してみます。
import React from 'react' import { myFeature, MyFeatureProps, WithMyFeature } from './my-feature' // ライブラリから RenderProp として機能が提供 export function FeatureFromRenderProp(props: MyFeatureProps) { return props.children(myFeature) } // RenderProp → HOC (*˘꒳˘*) すんなり export function withFeatureFromRenderProp<P extends {}>(C: React.ComponentType<P & WithMyFeature>) { return (props: P) => ( <FeatureFromRenderProp> {feature => <C feature={feature} {...props} />} </FeatureFromRenderProp> ) } // RenderProp → Hook (*˘꒳˘*) かけない... export function useFeatureFromRenderProp() { return null }
RenderProp → HOC への変換はかなり自然に書けている気がします。
これも残念ながら Hook はうまく組めませんでした。
Hook から変換する
最後に、ライブラリから Hook として機能が提供される場合、それを HOC や RenderProp として変換できないか試してみます。
import React, { useMemo } from 'react' import { myFeature, MyFeatureProps, WithMyFeature } from './my-feature' // ライブラリから Hook として機能が提供 export function useFeatureFromHook() { return useMemo(() => myFeature, []) } // Hook → HOC (*˘꒳˘*) すんなり export function withFeatureFromHook<P extends {}>(C: React.ComponentType<P & WithMyFeature>) { return (props: P) => { const feature = useFeatureFromHook() return <C feature={feature} {...props} /> } } // Hook →RenderProp (*˘꒳˘*) すんなり export function FeatureFromHook(props: MyFeatureProps) { const feature = useFeatureFromHook() return props.children(feature) }
Hook → HOC or RenderProp の変換は双方ともすんなり書けている気がします。
Hook 化で試したこと
無理やり組めばいけるんじゃないか、と思いクロージャを利用したアクセスで createElement
時に値の参照を得るコードを書いてみました。
export function useFeatureFromHoc() { let extractedFeature: MyFeature | undefined createElement(withFeatureFromHoc(({ feature }) => { extractedFeature = feature return null })) if (!extractedFeature) throw new Error('Feature unextracted') return extractedFeature }
しかしこの Hook をコンポーネント内で評価してもエラーが投げられるばかりで値は得られませんでした。 こんな感じで何とかこねくり回せばいける気がしなくはないのですが...
まとめ
- HOC → RenderProp: OK?, Hook: 無理
- RenderProp → HOC: OK, Hook: 無理
- Hook → HOC: OK, RenderProp: OK
Hook は RenderProp に変換可能で、それらは HOC に変換可能です。 変換の柔軟さでいうと Hook > RenderProp > HOC という感覚です。
- 一応 HOC を RenderProp 化することはできる
- でも書いていてちょっと不安になるコードになる
- RenderProp と HOC を Hook に変換する方法は見つからなかった
- なので Hook が提供されていないライブラリをの機能を自前で Hook に変換する、といったことはできなさそう、少なくともすんなりとは...
- 例えば react-router の
withRouter
とかをちょちょっとやってuseRouter
できた、みたいなことはできなさそう
- プロジェクト内で機能の切り出しを考える際は、まず Hook として切り出しておけば、RenderProp や HOC などの使い方をしたい場合にも数行のコードを追加すれば対応可能
仕事でコードを組んでいると複雑で「これ変換できるのかな...」としばらく試行錯誤することが多いのですが、一度シンプルな例で試しておくと、変換のパターンとその要所が把握できて多少理解が整理されますね。
でも正直このあたりの変換可能性はまだ全然自分の中で固まったイメージができていません。 なぜ変換可能性のグラフがこのようになるのか、の部分がです。
ものすごくふんわりした感覚でいうと Hook という枠組みは React Component の外にあり、そして React では一旦 Component の内部に入ってしまった値は外に出しにくい、という感覚があります。 React って動作原理は単純なのですが、その上で現れてくる使用感には単純にその動作原理に帰結させづらいものが多いです。 シンプルな枠組みの上に構築される多相な世界、という感じは数学とも似たところがありますね。
こういうところが React の面白さですね。