はじめに
この記事はJSL(日本システム技研) Advent Calendar 2021の記事です。
とある事情でReactの高階コンポーネントについて調べなければいけなくなったので、その過程でわかったことなどをまとめました。
いや難しいよ高階コンポーネント、、、
高階コンポーネントとは
React公式ドキュメント先生の説明がこちら
高階コンポーネント (higher-order component; HOC) はコンポーネントのロジックを再利用するための React における応用テクニックです。HOC それ自体は React の API の一部ではありません。HOC は、React のコンポジションの性質から生まれる設計パターンです。
一瞬?????となりました
が、よく読んでみると、どうやらいろんなコンポーネントで使っている同じようなロジックを共通化するための技術ということらしいです。
高階コンポーネント自体はHooksなどのReactの機能の一部として提供されているものではなく、あくまで設計技法のひとつだよと言っておられます。
なるほど?
わかったようなわからないような、、、
さらにReactさん(ドキュメントのこと)はこうも言っています。
高階コンポーネントとは、あるコンポーネントを受け取って新規のコンポーネントを返すような関数です。
つまるところ、高階コンポーネントとは関数のことらしいです。
コンポーネントを引数にとって、複数のコンポーネントで使いまわしたいロジックを加えた新しいコンポーネントを返す関数をReactの世界では高階コンポーネントと呼んでいるということのようです。
なんとなく理解できました。
じゃあいつ使うの
じゃあこの高階コンポーネント、いつ使うのかといいますと、Reactさん曰く、横断的関心事(複数コンポーネントにまたがって処理しなければいけないこと)を処理する時に使ってねとのことでした。
昔はミックスインというクラスの継承のような形で横断的関心事を処理することをお勧めしていたようなのですが、暗黙的な依存関係のせいでチーム開発が難しくなったり、ミックスインの名前の衝突(命名問題)の解決が難儀だったりとミックスインの弊害が色々出てきたようで現在は推奨されなくなったようです。(雑に訳したので間違ってたらすいません)
詳しくは以下参照
https://ja.reactjs.org/blog/2016/07/13/mixins-considered-harmful.html
じゃあ、まあ横断的関心事に高階コンポーネントを使えばいいのはわかったけど、今まであまりプロジェクトで使ってるの見たことないのだが、、、
と思いさらに調べてみると、どうやらHooksの台頭により現在は下火のようです。
従来のクラスコンポーネントでは、Stateやライフサイクルを扱う際に、constructor
やcomponentDidMount
、componentWillUnmount
等で似たような処理を複数コンポーネントにまたがって書いていたため、需要があったようです。
しかし、Hooksの登場により、現在はUseEffectやuseStateを使うだけで、高階コンポーネントを使わずともStateやライフサイクルを便利に扱えるようになり、使うケースが減ったとのこと。
前までは高階関数を使うようなケースでも、現在は大抵、Hooksとコンポジション(コンポーネント分割)で対応できるみたいですね。
半年前からこの仕事を始め、Hooksネイティブな自分があまり見たことないのが納得でした。
とりあえず雰囲気掴むために作ってみた
まあそれでも、Hooksとコンポジションで対応できないケースがあるかもしれないので、その時に備えて触ってみました。高階コンポーネントよ、いざ参る。
今回はコンポーネントにまたがる横断的関心事を表現するために、radioボタンの選択によってアプリの配色が変わるという想定で作りました。
親コンポーネントに、radioボタンがあり、propsで受け取ったstateの値によってbackgroundColorの色分けをします。
親コンポーネントのコードは以下(Styleはemotion、あとはTypeScriptを使っていますが、記事の本筋とは関係ないのでお気になさらず)
import "./styles.css";
import { css } from '@emotion/css'
import React, { useState } from 'react';
import ComponentA from './components/ComponentA'
import ComponentB from './components/ComponentB'
import ComponentC from './components/ComponentC'
export type GenderType = "male" | "female"
const componentWrapper = css({
display: "flex",
justifyContent: "center",
})
export default function App() {
const [gender, setGender] = useState<GenderType>()
const handleGender = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setGender(event.target.value as GenderType)
}
return (
<div className="App">
<input type="radio" name="gender" id="male"
value="male" onChange={handleGender}/>
<label id="male">男性</label>
<input type="radio" name="gender" id="female"
value="female" onChange={handleGender} />
<label id="female">女性</label>
<div className={componentWrapper}>
<ComponentA gender={gender}/>
<ComponentB gender={gender}/>
<ComponentC gender={gender}/>
</div>
</div>
);
}
そして、ComponentA、ComponentB、ComponentCでそれぞれで、genderによるbackgroundColorの出し分けをするのは嫌なので、高階コンポーネントを使ってみます。
コードはこちら
import { css } from '@emotion/css';
import React from 'react';
import { GenderType } from '../App';
const defaultStyle = css({
height: "50px",
margin: "10px",
padding: "3px"
})
const maleStyle = css(defaultStyle, {
backgroundColor: "#96d4fa",
})
const femaleStyle = css(defaultStyle, {
backgroundColor: "#faafd4",
})
export const withBackGroundColor = (
Component: React.ComponentType<{}>,
) => {
return (props: {gender: GenderType | undefined}) => {
if(!props.gender){
return (
<div className={defaultStyle}>
<Component />
</div>
)
}
return (
<>
{ props.gender === "male" ?
<div className={maleStyle}>
<Component/>
</div> :
<div className={femaleStyle}>
<Component/>
</div>
}
</>
)
}
}
高階コンポーネントはwithから始まる慣習があるらしいので、関数名をwithBackGroundColor
とし、コンポーネントを引数で受け取って、そのコンポーネントが受け取ったpropsのgenderによって、異なる背景色を持つdivでラップしたコンポーネントを返す関数を作りました。
あとはComponentA、ComponentB、ComponentCでこのwithBackGroundColor
をimportして、使ってあげるだけですね。
// ComponentA
import { withBackGroundColor } from '../HOC/WithBackGroundColor';
import { GenderType } from "../App"
const ComponentA = (props: {gender: GenderType | undefined}) => {
return (
<div>ComponentA</div>
)
}
export default withBackGroundColor(ComponentA)
// ComponentB
import { withBackGroundColor } from '../HOC/WithBackGroundColor';
import { GenderType } from "../App"
const ComponentB = (props: {gender: GenderType | undefined}) => {
return (
<div>ComponentB</div>
)
}
export default withBackGroundColor(ComponentB)
// ComponentC
import { withBackGroundColor } from '../HOC/WithBackGroundColor';
import { GenderType } from "../App"
const ComponentC = (props: {gender: GenderType | undefined}) => {
return (
<div>ComponentC</div>
)
}
export default withBackGroundColor(ComponentC)
こうすれば、ComponentA、ComponentB、ComponentCは色分けのロジックを分離でき、シンプルに作れます。(propsで渡した値で各子コンポーネントの配色を変えるという作りがいいかどうかはともかくとして)
おわりに
高階コンポーネントについて色々調べてきましたが、Hooks使って関数コンポーネントで実装しているプロジェクトであれば、使い所はそれほどなさそうだなと個人的には感じました。
今回のサンプル実装のケースならコンポジションで対応できますし、Hooksとコンポジションで対応できるケースであえて高階コンポーネントを使う理由も見当つきませんでした。
とはいえ高階コンポーネントが役立つケースもきっとある、と信じているので、引き続き追っていこうと思います。
こういう時便利だよ的なケースありましたら教えて欲しいです。
この記事は以上になります。
お読みいただきありがとうございました。