React の描画内容を Decorator でラッピングしてみる!
React 書いてますか?
最近私は Vue の方がお好みですが、
やっぱり複雑な事をしようと思うと、
JavaScript を直接拡張しているような React の書き味はなんとも心地よいものですね。
で、先日 React を書いていたのですが、
Nuxt の layout プロパティのように、メニューなどの共通で使う部品を外部から差し込みたいなと思いまして。
TypeScript で書いていたので、 Decorator で書けば良いじゃん!
ということでやってみた記録です。
Decorator でコンテンツラッパーを作る
やりたいこと
やりたいことはこれ。
const menu: FC = ({ children }) => (
<div>
<ul>
<li>項目1</li>
</ul>
{ children }
</div>
);
const main: FC = () => (
<Menu>
<p>main contents</p>
</Menu>
);
こうすると、main が menu の children で展開されるので、
メインコンテンツにメニューをくっつけて表示することができますね。
ただ、これだとメニューの中にメインコンテンツがあるみたいな DOM 構成になって
ちょっと気持ち悪い感じ。
ということで、この menu を、デコレーターでどうにかくっつけたいと言うのが今回のお気持ち。
デコレーターで書き直した Ver
さっきは省略しましたけど、とりあえず HTML 側にこんな感じのタグが用意されている想定をして。
<div id=”app”></div>
実際のコードはこちら。
@menu
class App extends React.Component {
render() {
return (
<h1>
main
</h1>
);
}
}
ReactDOM.render(<App />, document.getElementById(’app’));
function menu(target, name) {
const originalRender = target.prototype.render;
target.prototype.render = function() {
return (
<div>
<ul>
<li>項目1</li>
</ul>
{ originalRender() }
</div>
);
};
}
menu 関数みてもらったら、とってもシンプルに作れることが分かりますね。
元々の render の内容は target.prototype.render に入ってくるので、
一旦それを originalRender 変数に待避させ、
target.prototype.render に新しく描画する内容を上書きしています。
元々の内容を originalRender に入れてあるので、
menu で元の内容を描画させたいところに { originalRender() } を置いておけば
そこに元々の内容が展開されますね。
とっても簡単。
Functional Component の場合どうするの?
実はデコレーターを使った Ver に書き直したときに、
しれっと元の方を class を使った Component に差し替えてました。
元のコードではどちらも Functional Component をつかってたはず…
では Functional Component にも対応できるようにデコレーターを書き直そう!となるのですが、
残念なことに、
@menu const app = () => ( <h1>App</h1> );
みたいな、単純な変数にデコレーターはつけられないのです。
(クラスの中のプロパティならできるのですけどね。)
ということで、ここで HOC (Higher-order Component) に頼りましょう。
HOC で書き直す
とはいっても、結局やることはほぼ一緒で、クラスの場合は先ほどのようにデコレーターも使えます。
単純に、デコレーター内で関数を展開して render を走らせていたものを、
新しい render を含む Component を返すようにしてあげたらいいだけです。
ということで。
export menu function (TargetComponent: any) {
return class Menu extends Component<any, any> {
render() {
return (
<div>
<Menu />
<TargetComponent {...this.props}/>
</div>
);
}
}
};
こんな感じに。
実際に使うときは、
const dashboard: FC<HelloProps> = () => (
<h1>
Hello!
</h1>
);
export default menu(dashboard);
こんな感じで、 Functional Component を HOC でラップしてあげたら OK です。
ちなみに、先ほど同様
@menu
class App extends React.Component {
...
みたいな使い方をしても、同じように動くようになっています。
(処理を追いかけてみたら、やっていることは同じであることが分かります!)
レイアウトのような、使い回せる部品は HOC に出すことを検討しよう!
以外に難しそうで、単純に関数をラッピングしてあげるだけなので
分かってしまえば簡単ですね。
HOC はちょっと手を出しづらい記法ですが、一度慣れるととても便利なものなので、
是非こういう簡単なところからつかっていってみたいですね。