hoc-pattern について
コンポーネント間でロジックを再利用する高階コンポーネントを解説する React スキルです。HOC の合成、props の衝突、Hooks との使い分けを扱います。
主な機能
- 01再利用可能な振る舞いでコンポーネントをラップ
- 02複数の HOC の合成
- 03ラップ先へのデータと props の受け渡し
- 04props 名の衝突回避
- 05HOC と React Hooks の比較
活用シーン
- 01複数コンポーネントへの共通スタイル適用
- 02認可やグローバル状態ロジックの再利用
- 03データ駆動コンポーネントへの読み込み表示追加
- 04既存の React コードでの HOC の評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill hoc-patternインストール後、Claude Code で/hoc-patternを入力して読み込まれたか確認します。
詳細
HOC パターン
目次
アプリケーションでは、同じロジックを複数のコンポーネントで使用したいことがよくあります。このロジックには、コンポーネントへの特定のスタイルの適用、認証の必須化、グローバル状態の追加などが含まれます。
複数のコンポーネントで同じロジックを再利用する方法のひとつが、高階コンポーネントパターンです。このパターンを使うと、アプリケーション全体でコンポーネントのロジックを再利用できます。
使用する場面
- カスタマイズされていない同じ動作を多くのコンポーネントに適用する必要がある場合に使用します
- 追加のカスタムロジックなしで、コンポーネントを単独で動作させる必要がある場合に便利です
使用すべきでない場面
- カスタムフックを使えば、ネストを減らし、可読性を高めて同じ結果を得られる場合
- フックが状態を持つロジックを共有する標準的な方法である、React 18 以降の新しいコードの場合
- HOC ラッパーによってプロパティ名の衝突が発生したり、DevTools でコンポーネントツリーが分かりにくくなったりする場合
手順
- コンポーネントを受け取り、動作を拡張した新しいコンポーネントを返す関数を作成します
- HOC 内でプロパティ名を変更またはマージし、名前の衝突を避けます
- ラッパーの過剰な使用や深いネストを避けるため、ほとんどの新しいコードでは HOC より React Hooks を優先します
- 複数の HOC を合成する場合は慎重に行い、合成の順序が重要であることに注意します
詳細
高階コンポーネント(HOC)とは、別のコンポーネントを受け取るコンポーネントです。HOC には、引数として渡したコンポーネントに適用するロジックが含まれています。そのロジックを適用した後、HOC は追加のロジックを備えた要素を返します。
アプリケーション内の複数のコンポーネントに、常に特定のスタイルを適用したいとします。毎回ローカルで style オブジェクトを作成する代わりに、渡されたコンポーネントに style オブジェクトを追加する HOC を作成できます。
function withStyles(Component) {
return props => {
const style = { padding: '0.2rem', margin: '1rem' }
return <Component style={style} {...props} />
}
}
const Button = () => <button>Click me!</button>
const Text = () => <p>Hello World!</p>
const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)
これで、StyledButton コンポーネントと StyledText コンポーネントを作成できました。これらは Button コンポーネントと Text コンポーネントを変更したものです。どちらにも、withStyles HOC で追加したスタイルが適用されています。
ユーザーエクスペリエンスをもう少し改善しましょう。データを取得している間、ユーザーに "Loading..." 画面を表示したいとします。DogImages コンポーネントに直接データを追加する代わりに、このロジックを追加する高階コンポーネントを使用できます。
withLoader という HOC を作成しましょう。HOC はコンポーネントを受け取り、そのコンポーネントを返します。この場合、withLoader HOC は、データの取得が完了するまで Loading… を表示する要素を受け取ります。
function withLoader(Element) {
return (props) => <Element />;
}
ただし、受け取った要素をそのまま返すわけではありません。代わりに、データの読み込みがまだ続いているかどうかを判定するロジックを、この要素に追加します。
withLoader HOC を広く再利用できるようにするため、コンポーネント内に Dog API の URL をハードコードすることはしません。代わりに、URL を引数として withLoader HOC に渡せます。これにより、別の API エンドポイントからデータを取得する間に読み込みインジケーターが必要な、どのコンポーネントにもこのローダーを使用できます。
function withLoader(Element, url) {
return (props) => {};
}
HOC は、この場合は関数コンポーネント props => {} である要素を返します。データの取得中に Loading… のテキストを表示できるロジックを、この要素に追加します。データの取得が完了すると、コンポーネントは取得したデータをプロパティとして渡します。
これで、任意のコンポーネントと URL を受け取れる HOC を作成できました。
useEffectフック内で、withLoaderHOC はurlの値として渡された API エンドポイントからデータを取得します。データがまだ返されていない間は、Loading...テキストを含む要素を返します。- データの取得が完了すると、取得したデータを
dataに設定します。dataはnullではなくなったため、HOC に渡した要素を表示できます。
では、この動作をアプリケーションに追加するにはどうすればよいでしょうか。DogImages.js では、プレーンな DogImages コンポーネントをそのままエクスポートするのはやめます。代わりに、DogImages コンポーネントを withLoading HOC で「ラップ」してエクスポートします。
export default withLoader(
DogImages,
"https://dog.ceo/api/breed/labrador/images/random/6"
);
高階コンポーネントのパターンを使うと、ロジックを 1 か所にまとめたまま、複数のコンポーネントに同じロジックを適用できます。withLoader HOC は、受け取るコンポーネントや URL を問いません。有効なコンポーネントと API エンドポイントであれば、その API エンドポイントから取得したデータを、渡されたコンポーネントに渡します。
合成
複数の高階コンポーネントを合成することもできます。たとえば、ユーザーが DogImages リストにカーソルを合わせたときに Hovering! というテキストボックスを表示する機能も追加したいとします。
渡された要素に hovering プロパティを提供する HOC を作成する必要があります。このプロパティに基づいて、ユーザーが DogImages リストにカーソルを合わせているかどうかに応じて、テキストボックスを条件付きで表示できます。
これで、withLoader HOC を withHover HOC でラップできます。
DogImages 要素には、withHover と withLoader の両方から渡されたすべてのプロパティが含まれるようになりました。これで、hovering プロパティの値が true か false かに応じて、Hovering! テキストボックスを条件付きで表示できます。
HOC の合成に使われる有名なライブラリに、recompose があります。HOC は React Hooks でほぼ置き換えられるため、recompose ライブラリは現在メンテナンスされていません。
フック
場合によっては、HOC パターンを React フックに置き換えられます。
withHover HOC を useHover フックに置き換えてみましょう。高階コンポーネントを用意する代わりに、要素に mouseOver イベントリスナーと mouseLeave イベントリスナーを追加するフックをエクスポートします。HOC の場合のように、要素を渡すことはできません。その代わり、mouseOver イベントと mouseLeave イベントを受け取る要素に設定する ref をフックから返します。
useEffect フックはコンポーネントにイベントリスナーを追加し、ユーザーが現在その要素にマウスを合わせているかどうかに応じて、hovering の値を true または false に設定します。フックからは ref と hovering の両方を返す必要があります。ref は mouseOver イベントと mouseLeave イベントを受け取るコンポーネントに ref を設定するために、hovering は Hovering! テキストボックスを条件に応じてレンダリングするために必要です。
DogImages コンポーネントを withHover コンポーネントでラップする代わりに、コンポーネント内で useHover フックを直接使えます。
一般的に言えば、React フックは HOC パターンに取って代わるものではありません。
「ほとんどの場合、フックで十分であり、ツリーのネストを減らすのに役立ちます。」 - React ドキュメント
React ドキュメントにあるように、フックを使うとコンポーネントツリーの深さを減らせます。HOC パターンを使うと、コンポーネントツリーが深くネストされやすくなります。
<withAuth>
<withLayout>
<withLogging>
<Component />
</withLogging>
</withLayout>
</withAuth>
コンポーネントにフックを直接追加することで、コンポーネントをラップする必要がなくなります。
高階コンポーネントを使うと、ロジックを一か所にまとめたまま、多くのコンポーネントに同じロジックを提供できます。一方、フックを使うとコンポーネント内からカスタムの動作を追加できます。ただし、複数のコンポーネントがこの動作に依存している場合、HOC パターンに比べてバグが発生するリスクが高まる可能性があります。
HOC の最適な使用例:
- _同じで、カスタマイズされていない_動作を、アプリケーション全体の多くのコンポーネントで使う必要がある場合。
- 追加のカスタムロジックがなくても、コンポーネントが単独で動作できる場合。
フックの最適な使用例:
- その動作を使う各コンポーネントに合わせてカスタマイズする必要がある場合。
- 動作がアプリケーション全体に広がっておらず、1つまたは少数のコンポーネントだけが使う場合。
- 動作によってコンポーネントに多くのプロパティが追加される場合
ケーススタディ
HOC パターンに依存していたライブラリの中には、リリース後にフックのサポートを追加したものがあります。その好例が Apollo Client です。
Apollo Client を使う方法の1つは、graphql() 高階コンポーネントを使うことです。graphql() HOC を使うと、クライアントのデータを、その高階コンポーネントでラップされたコンポーネントから利用できるようになります。現在も graphql() HOC は使えますが、いくつかデメリットがあります。
コンポーネントが複数のリゾルバーにアクセスする必要がある場合、そのために複数の graphql() 高階コンポーネントを合成する必要があります。複数の HOC を合成すると、データがコンポーネントにどのように渡されているのかがわかりにくくなることがあります。場合によっては HOC の順序が重要になるため、コードのリファクタリング時に簡単にバグにつながる可能性があります。
フックのリリース後、Apollo は Apollo Client ライブラリにフックのサポートを追加しました。開発者は graphql() 高階コンポーネントを使う代わりに、ライブラリが提供するフックを通じてデータに直接アクセスできるようになりました。
useMutation フックを使うことで、コンポーネントにデータを提供するために必要なコード量を減らせました。
定型コードが減るだけでなく、コンポーネント内で複数のリゾルバーのデータを使うのもずっと簡単です。複数の高階コンポーネントを合成する代わりに、コンポーネント内に複数のフックを記述するだけで済みます。この方法なら、データがコンポーネントにどのように渡されるかをより簡単に把握でき、コンポーネントをリファクタリングしたり、小さなパーツに分割したりするときの開発者体験も向上します。
メリット
高階コンポーネントのパターンを使用すると、再利用したいロジックをすべて一か所にまとめられます。これにより、コードを何度も複製してアプリケーション全体にバグを誤って広げたり、そのたびに新たなバグを持ち込んだりするリスクを減らせます。ロジックを一か所にまとめることで、コードを DRY に保ち、関心事の分離を簡単に徹底できます。
デメリット
HOC が要素に渡す props の名前は、名前の衝突を引き起こす可能性があります。
function withStyles(Component) {
return props => {
const style = { padding: '0.2rem', margin: '1rem' }
return <Component style={style} {...props} />
}
}
const Button = () => <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)
この場合、withStyles HOC は渡された要素に style という prop を追加します。しかし、Button コンポーネントにはすでに style という prop があり、上書きされてしまいます。prop の名前を変更するか、props をマージすることで、HOC が意図しない名前の衝突に対応できるようにしてください。
function withStyles(Component) {
return props => {
const style = {
padding: '0.2rem',
margin: '1rem',
...props.style
}
return <Component style={style} {...props} />
}
}
const Button = () => <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)
複数の HOC を合成して使い、それぞれが内部でラップされている要素に props を渡す場合、どの HOC がどの prop を担当しているのかを把握するのが難しくなります。そのため、デバッグやアプリケーションの容易なスケーリングが妨げられることがあります。