投稿日:2021年2月16日
HOC(Higer Order Component)とはコンポーネントごとの共通のロジックをまとめるコンポーネントの設計手法のことをいいます。
HOC(Higer Order Component)とはコンポーネントごとの共通のロジックをまとめるコンポーネントの設計手法のことをいいます。コンポーネント版のミックスインのようなものをイメージするといいでしょう。
あくまで推奨される設計手法であって、ライブラリなどが提供されているわけではないことには注意しましょう。
React16.8以降のバージョンではコンポーネントの機能のロジックを再利用するためのHooksという機能が追加されました。ロジックの再利用はできる限りこちらのHooksで実装するようにして、Hooksでどうにもできないような場合にだけHOCを使うようにしましょう。
この記事のコードは以下の環境で確認されました。
簡単なHOCコンポーネントを作成してみます。
HOCの関数はwith〇〇のような名前にするのが慣例になっています。ここではwithSimpleという名前にしてみます。
import React from "react";
/**
* withPropsで拡張するprops
*/
export interface InjectedProps {
themeColor: string;
}
/**
* コンポーネントを受け取ってthemeColorのpropsを追加する
* @param WrappedComponent 拡張したいコンポーネント
* @returns {React.FC<Omit<P, keyof InjectedProps>>}
*/
export const withSimple = <P extends InjectedProps = InjectedProps>(
WrappedComponent: React.ComponentType<P>
) => {
return (props: Omit<P, keyof InjectedProps>) => {
return (
<WrappedComponent
{...{ themeColor: "#aaaaaa" }}
{...(props as P)}
></WrappedComponent>
);
};
};
簡単に解説をしていきます。
InjectedPropsはHOCで拡張するpropsのinterfaceの定義です。
withSimple()関数はGeneric型でinterfaceを受け取りInjectedPropsで拡張します。この型は引数として受け取るコンポーネントのpropsの型になります。今回のように利用のしやすさのためにデフォルトの型をしておくと便利ですね。
引数として受け取るコンポーネントはClassコンポーネントとFunctionalコンポーネントの両方が想定できます。React.ComponentType<P>はこのどちらにも対応できるGenericの型です。
次に返り値についてですが、今回は特にメソッドを定義したりしないので、Functionalコンポーネントを返します。返すコンポーネントのpropsの型は引数として受け取ったコンポーネントのpropsの型からInjectedPropsの型を除いています。このおかげでwithSimpleで拡張されたコンポーネントはthemeColorの指定をする必要がなくなっています。interfaceの除外にはTypeScriptのユーティリティのOmit<T>を使います。
JSXでのWrappedComponentのpropsの渡し方には注意点があります。propsのP型は呼び出し時のGenericの型指定によって変わるため、asで型を指定した後スプレッドで展開する必要があります。また今回のようにデフォルトの値を渡す場合、必ずオブジェクトにしてスプレッドで展開する式にするしなければいけません。
利用する側のコンポーネントを書いてみます。
import { InjectedProps, withSimple } from "../hoc/withSimple";
interface Props {
text: string;
}
const SampleComponent = (props: InjectedProps & Props) => {
return (
<button style={{ backgroundColor: props.themeColor }}>{props.text}</button>
);
};
export default withSimple<InjectedProps & Props>(SampleComponent);
利用する場合はHOCを定義する際に利用したPropsのInterfaceであるInjectedPropsで対象のPropsを拡張するのを忘れないようにしましょう。
拡張したコンポーネントは以下のようにthemeColorの指定なしで利用できます。
import React from "react";
import SampleComponent from "./components/SampleComponent";
function App() {
return (
<div>
<SampleComponent text="Hello" />
</div>
);
}
export default App;
ちなみにHOCでメソッドの拡張などをしたい場合には下のようにクラスコンポーネントを返すといいでしょう。
import React from "react";
/**
* withPropsで拡張するprops
*/
export interface InjectedProps {
themeColor: string;
}
/**
* コンポーネントを受け取ってthemeColorのpropsを追加する
* @param WrappedComponent 拡張したいコンポーネント
* @returns {React.FC<Omit<P, keyof InjectedProps>>}
*/
export const withSimple = <P extends InjectedProps = InjectedProps>(
WrappedComponent: React.ComponentType<P>
) => {
return class extends React.Component<Omit<P, keyof InjectedProps>, {}> {
render() {
return (
<WrappedComponent
{...{ themeColor: "#aaaaaa" }}
{...(this.props as P)}
></WrappedComponent>
);
}
};
};