対象読者
- HOCをよく聞くので概要をサクッと理解したい
- でもReactの公式Document(Higher-Order Components - React)が英語で読むの面倒だ
HOCは高階関数である
まずHigher-Order Component(以下HOC)が何であるかを説明すると、
HOCは「コンポーネントを引数にとってコンポーネントを返す関数」のこと。
そして、HOCの名前の一部であるHigher-Orderとは高階関数の意味で、
高階関数とは「関数を引数、戻り値として扱う関数」のこと。
それがなぜ「コンポーネントを引数にとってコンポーネントを返す関数」だと言えるのかと言うと、
コンポーネントが関数だから。
なぜなら、コンポーネントはPropsを引数に受け取って仮想DOMの要素を返すから。
だから、HOCはコンポーネントを扱う高階関数になる。
HOC: (Component) => NewComponent;
何が嬉しいのか?
では何のためにそんな関数が存在するのかと言うと、複数のコンポーネントに共通する振る舞いを一つにまとめるため。
他の言語のモジュール(クラス間での共通の振る舞いをまとめるもの)に考え方が近い。
共通の振る舞いを一つにまとめることで、コンポーネント間のコードの重複を排除したり、そこだけを再利用することが可能になる。
class ComponentA extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(e) {
this.setState({
x: e.clientX,
y: e.clientY
});
}
render() {
return (
<div onMouseMove={this.handleMouseMove}>
<p>Current X position is {this.state.x}</p>
<p>Current Y position is {this.state.y}</p>
</div>
)
}
}
class ComponentB extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(e) {
this.setState({
x: e.clientX,
y: e.clientY
});
}
render() {
return (
<div onMouseMove={this.handleMouseMove}>
<p>Current position is ({this.state.x}, {this.state.y})</p>
</div>
)
}
}
画面の位置情報のState({ x: 0, y: 0 }
)と画面上でマウスが動いた時のコールバック関数(handleMouseMove
)をHOCで共通化できる。
const WithMousePosision = (WrappedComponent: *) => class _ extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(e) {
this.setState({
x: e.clientX,
y: e.clientY
});
}
render() {
return (
<div onMouseMove={this.handleMouseMove}>
<WrappedComponent
x={this.state.x}
y={this.state.x}
{...this.props}
/>
</div>
)
}
};
// component_a.js
const ComponentA = (props) => {
<p>Current position is ({props.x}, {props.y})</p>
};
export default WithMousePosision(ComponentA);
// component_b.js
const ComponentB = (props) => {
return [
<p>Current X position is {props.x}</p>,
<p>Current Y position is {props.y}</p>
]
};
export default WithMousePosision(ComponentB);
継承との違い
Reactの公式Documentのここを見ると、facebookでは継承は全く使われていない様子。
先ほどの例を継承を使って書いて見る。
class WithMousePosision extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(e) {
this.setState({
x: e.clientX,
y: e.clientY
});
}
render() {
return <div/>;
}
}
class ComponentA extends WithMousePosision {
render() {
return (
<div onMouseMove={this.handleMouseMove}>
<p>Current X position is {this.state.x}</p>
<p>Current Y position is {this.state.y}</p>
</div>
)
}
}
class ComponentB extends WithMousePosision {
render() {
return (
<div onMouseMove={this.handleMouseMove}>
<p>Current position is ({this.state.x}, {this.state.y})</p>
</div>
)
}
}
HOCの継承と異なる点は、
- 引数になるコンポーネントに対してHOCの内部実装は隠れる、そのため上書きされるような副作用が起こらない(正しく書けば)
- StatelessFunctionalComponent(SFC)に対しても使える
例えば、上記の例で説明すると継承の場合はComponentB
でWithMousePosision#handleMouseMove
を簡単に上書きできてしまうが、HOCの場合はできない。またサブクラスである、ComponentA
をSFCにすることはできない。
使用上の注意点
1. 純粋関数(同じ入力であれば同じ出力になる関数)である必要がある
引数で渡されたコンポーネントに破壊的な変更をすると、そのコンポーネントはHOCを使わなくてもその振る舞いを持ってしまう。
元のComponentに期待された振る舞いの同等性が失われると言う意味で、オブジェクト指向設計の原則の一つであるリスコフの置換原則(スーパークラスが満たす振る舞いはサブクラスでも満たさないといけない)に違反するのと似ている。一度、それが破られるとHOCを使ってどのように振る舞いが変わるのか実装を見て、毎回チェックしなくてはいけなくなってしまう。
また、引数で渡されたコンポーネントに破壊的な変更をしても、さらに別のHOCで破壊的な変更が上書きされてしまう可能性もある。
There are a few problems with this. One is that the input component cannot be reused separately from the enhanced component. More crucially, if you apply another HOC to EnhancedComponent that also mutates componentWillReceiveProps, the first HOC’s functionality will be overridden! This HOC also won’t work with functional components, which do not have lifecycle methods. 参照
2. Renderメソッド内でHOCを使わない
実際にDOM要素を更新して、再描画するかどうかは、Render内での差分によって仮想DOMが決めている。
しかし、Render内でHOCを作り、毎回新しい参照のComponentができると、差分が生じ、そのコンポーネント以下のツリーは全て毎回再描画されることになる。
型定義
色々書き方があるが、結構複雑になりがち。
import react from 'react';
import type ComponentType from 'react';
import type HOC from 'recompose';
// 受け取るPropsの型
type OuterPoprs = {
...
};
// 内部のStateの型
type InnerState = {
...
};
// コンポーネントに渡す型
type Props = OuterProps & InnerState & { ... };
const someHoc: HOC<Props, OuterProps> = (WrappedComponent: ComponentType<Props>) => {
return class _ extends React.Component<OuterProps, InnerState> {
// 何かしらの共通の処理
render() {
return <WrappedComponent ... />
}
}
};