一、はじめに#
高階関数(Higher-order function)は、次のいずれかの条件を満たす関数です。
- 1 つ以上の関数を入力として受け取る
- 関数を出力する
React
では、高階コンポーネントは、1 つ以上のコンポーネントを引数として受け取り、コンポーネントを返すものであり、本質的にはコンポーネントではなく関数です。
const EnhancedComponent = highOrderComponent(WrappedComponent);
上記のコードでは、この関数はコンポーネントWrappedComponent
を引数として受け取り、加工された新しいコンポーネントEnhancedComponent
を返します。
このような高階コンポーネントの実装方法は、本質的にはデコレーターパターンです。
二、実装方法#
基本的な高階コンポーネントの実装テンプレートは次のようになります。
import React, { Component } from 'react';
export default (WrappedComponent) => {
return class EnhancedComponent extends Component {
// 何かしらの処理
render() {
return <WrappedComponent />;
}
}
}
渡された元のコンポーネントWrappedComponent
に対して、必要な操作(プロップスの操作、ステートの抽出、他の要素でのラップなど)を行い、必要なコンポーネントEnhancedComponent
を作成します。
高階コンポーネントに共通のロジックを配置し、コンポーネントに一貫した処理を実装することで、コードの再利用性を実現します。
したがって、高階コンポーネントの主な機能は、コンポーネントの共通ロジックをカプセル化し、コンポーネント間で共通のロジックをより良く再利用することです。
ただし、高階コンポーネントを使用する際には、通常、次のような規則に従うことが一般的です。
- プロップスを一貫させる
- 関数コンポーネント(ステートレスコンポーネント)では ref 属性を使用しないでください。なぜなら、それにはインスタンスがないからです。
- 元のコンポーネント
WrappedComponent
をいかなる方法でも変更しないでください。 - 関連のないプロップス属性をラップされたコンポーネント
WrappedComponent
に透過的に渡す - render () メソッド内で高階コンポーネントを使用しないでください。
- compose を使用して高階コンポーネントを組み合わせる
- デバッグしやすいように表示名をラップしてください。
ここで注意する必要があるのは、高階コンポーネントはすべてのプロップスを渡すことができますが、ref を渡すことはできないということです。
高階コンポーネントに ref を追加する場合、ref はラップされたコンポーネントではなく、最も外側のコンテナコンポーネントのインスタンスを指します。ref を渡す必要がある場合は、React.forwardRef
を使用します。例:
function withLogging(WrappedComponent) {
class Enhance extends WrappedComponent {
componentWillReceiveProps() {
console.log('Current props', this.props);
console.log('Next props', nextProps);
}
render() {
const {forwardedRef, ...rest} = this.props;
// forwardedRefをrefに割り当てる
return <WrappedComponent {...rest} ref={forwardedRef} />;
}
};
// React.forwardRefメソッドは、propsとrefの2つの引数をコールバック関数に渡します
// したがって、ここでのrefはReact.forwardRefによって提供されます
function forwardRef(props, ref) {
return <Enhance {...props} forwardRef={ref} />
}
return React.forwardRef(forwardRef);
}
const EnhancedComponent = withLogging(SomeComponent);
三、応用シーン#
上記の理解に基づいて、高階コンポーネントはコードの再利用性と柔軟性を向上させることができます。実際のアプリケーションでは、権限制御、ログ記録、データ検証、例外処理、統計レポートなど、コアビジネスとは関係のないが複数のモジュールで使用される機能に使用されることがよくあります。
例えば、キャッシュからデータを取得してレンダリングする必要があるコンポーネントが存在するとします。通常の場合、次のように書くことがあります。
import React, { Component } from 'react'
class MyComponent extends Component {
componentWillMount() {
let data = localStorage.getItem('data');
this.setState({data});
}
render() {
return <div>{this.state.data}</div>
}
}
上記のコードはもちろんこの機能を実現できますが、他のコンポーネントにも同様の機能がある場合、各コンポーネントでcomponentWillMount
内のコードを繰り返し書く必要があります。これは明らかに冗長です。
次に、高階コンポーネントを使用して書き換えることができます。例:
import React, { Component } from 'react'
function withPersistentData(WrappedComponent) {
return class extends Component {
componentWillMount() {
let data = localStorage.getItem('data');
this.setState({data});
}
render() {
// {...this.props}を使用して、現在のコンポーネントに渡されたプロパティをラップされたコンポーネントWrappedComponentにも渡す
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
class MyComponent2 extends Component {
render() {
return <div>{this.props.data}</div>
}
}
const MyComponentWithPersistentData = withPersistentData(MyComponent2)
また、コンポーネントのレンダリングパフォーマンスの監視などもあります。例:
class Home extends React.Component {
render() {
return (<h1>Hello World.</h1>);
}
}
function withTiming(WrappedComponent) {
return class extends WrappedComponent {
constructor(props) {
super(props);
this.start = 0;
this.end = 0;
}
componentWillMount() {
super.componentWillMount && super.componentWillMount();
this.start = Date.now();
}
componentDidMount() {
super.componentDidMount && super.componentDidMount();
this.end = Date.now();
console.log(`${WrappedComponent.name} コンポーネントのレンダリング時間は ${this.end - this.start} msです`);
}
render() {
return super.render();
}
};
}
export default withTiming(Home);