Reactの機能の一つ、HOC(Higher-Order Component)についての説明をする。
これの理解により、適切なパフォーマンスチューリングや、記述量の削減に大きく貢献できる。
HOCとは?
以下のようなコードについて考える。
class A extends React.Component{
render(){
return (
<div>Hello, world</div>
);
}
}
export default A;
import A from 'A.jsx';
class B extends React.Component{
render(){
return (
<A />
);
}
}
ここで、<B />
の出力結果は、当然Hello, world
になる。
ここで、以下のような関数を用意する。
const sayHi = WrappedCompoent => {
return class extends WrappedCompoent{
componentWillMount(){
console.log('Hi!');
}
}
}
これを、A
に適用してみる。
class A extends React.Component{
render(){
return (
<div>Hello, world</div>
);
}
}
export default sayHi(A);
ここで、<B />
を評価すると、コンソールにHi!
と表示されるようになる。
sayHi
関数について考えてみる。
sayHi
関数は、コンポーネントを受け取って、コンポーネントを返す関数となっている。
渡されたコンポーネントクラスを継承し、メソッドをオーバーライドするクラスを返すことによって、「機能を拡張」している。
このように、「コンポーネントを受け取り、コンポーネントを返す関数」を、HOC(Higher-Order Component)という。
(「『コンポーネントを受け取り、コンポーネントを返す関数』により、拡張されたコンポーネント」の方をHOCと呼ぶ方が語彙的には適切だと思うが、公式ドキュメントの方では上のような定義になっている。ここでは便宜上、公式の方の定義を参照する。)
なお、sayHi
関数の機能は、次のようにして実現することもできる。
const say = message => WrappedCompoent => {
return class extends WrappedCompoent{
componentWillMount(){
console.log(message);
}
}
}
...
export default say('Hi!')(A)
このように、HOCは高階関数として提供されることが多い。
詳しくは、公式ドキュメント
コンポーネントコンポジションとの違い
コンポーネントの機能の拡張には、コンポーネントコンポジション(OOPでいうところのDecoratorパターン)が使われることが多い。
例えば、文字のリストを表示する<TextList />
に、スクロールバーを実装する場合は次のようになる。
<ScrollBar>
<TextList />
<ScrollBar />
ここで、<ScrollBar />
は<TextList />
の実装に依存しない(カプセル化を壊さない)ので、他のコンポーネントにもすぐに容易に実装できる。
これに対し、HOCはクラス継承を行うことにより実現できる、つまりHOCの対象となるコンポーネントの実装に依存する(カプセル化を壊す)
拡張対象のコンポーネントの実装に依存するか、しないかでコンポジションとHOCは使い分ける必要がある。
拡張性を高めるため、基本的にはコンポジションの方を優先する。
参考: Composition vs Inheritance
Recomposeの導入
HOC周りのヘルパメソッドを提供する、Recomposeライブラリを導入。
これのわかりやすい日本語のドキュメントはここ
例えば、compose
関数なら、
今まで以下のようになっていたものを、
HOC1(HOC2(HOC3(Component)))
次のようにできる。
const Enhance = compose(
HOC1,
HOC2,
HOC3
);
Enhance(Component)
HOCの使用例
React-Redux
ReactとReduxを組み合わせて使う場合、
Reactの各コンポーネントのprops
と、Reduxのstate
(ReactのStateと混同しないように!) とaction creator
を繋げる必要がある。
ここで、
- Reduxの
state
をprops
に反映するmapStateToProps
関数 - Reduxの
action creator
をprops
に反映するmapDispatchToProps
関数 - それらを繋げる
connect
関数
を用意し、それらを組み合わせてHOCとして適用する必要がある。
例えば、<A />
に対し、Reduxのstate
、action creator
を繋げる場合は、次のようになる。
connect(mapStateToProps, mapDispatchToProps)(A)
mapStateToProps
mapStateToProps
はReduxのstate
を受け取り、props
に追加したいstate
を返す関数である。
例えば、state
内のgender
をprops
に反映する場合は、mapStateToProps
は次のようになる
const mapStateToProps = state => ({
gender: state.gender
});
これにより、コンポーネント内ではthis.props.gender
としてアクセスできるようになる。
ところで、プロジェクト内の既存のコードだと、次のようになっている部分が多い
const mapStateToProps = state => {
return state;
};
しかし、これだとComponent側のpropsにstateの情報が全て反映されてしまい、関係のないstateが変わるたびにそのコンポーネントの描画処理が行われるため、パフォーマンスが悪化してしまう。
なので、上のように必要なものだけpropsに反映するようにする必要がある。
なお、mapStateToPropsを二重で適用したりする際などに、現在のpropsを参照する場合は、第二引数ownProps
を宣言する。
const mapStateToProps = (state, ownProps) => ({
...ownProps,
gender: state.gender
});
mapDispatchToProps
mapDispatchToProps
は、Reduxのdispacther
を受け取り、props
に追加したいaction creator
を返す関数である。
setGenderをpropsに渡す場合は、mapDispatchToPropsは次のようになる
import { setGender } from 'actions.jsx';
const mapDispatchToProps = dispatch => {
return {
actions: {
setGender: gender => dispatch(setGender(gender))
}
};
};
これは、bindActionCreatorsというヘルパ関数を使うことにより、次のように書ける。
const mapDispatchToProps = dispatch => {
return {
actions: bindActionCreators({ setGender }, dispatch)
};
};
これにより、コンポーネント内ではthis.props.actions.setGender
としてアクセスできる。
ところで、プロジェクト内の既存のコードだと、次のようになっている部分が多い
const mapDispatchToProps = dispatch => {
return {
actions: bindActionCreators(Actions, dispatch)
};
};
これも同じく、不要なaction creatorをpropsに渡してしまっているので、パフォーマンスの低下に繋がる。
なお、mapStateToPropsを二重で適用したりする際などに、現在のpropsを参照する場合は、第二引数ownProps
を宣言する。
const mapDispatchToProps = (dispatch, ownProps) => {
return {
actions: {
...ownProps.actions,
...bindActionCreators({ setGender }, dispatch);
};
};
}