ReactのonClick
イベントが設置されているコンポーネントをダブルクリックすると、シングルクリックイベントが立て続けに2回実行されます。
ちょっとそれでは困る場合の為に、連続したクリックを1回にまとめるコンポーネントを作成しました。
また、複数個所で気軽に実装できるようHigher-Order Component (HOC)
の形式にしてみました。
Higher-Order Components とは
APIとして提供されている機能ではなく、コンポーネントに関するロジックを再利用するためのテクニックの一つです。
クラスの継承よりもミックスインに近い印象ですが、ロジックがより隠蔽されています。
本家サイトによるとミックスインは混乱の元になるそうで、Composition
もしくはHOC
の利用が推奨されるそうです。
クラスの継承はFaceBook内では導入事例が無いとのこと。
JavaScriptはオーバーライドを防ぐ仕組みが無いので、事故を防ぐには運用ルールやコメントなどによるカバーが必要になってくるからでしょうか。
また、継承ではFunctionコンポーネントも使えませんね。
Higher-Order Components | React
Composition vs Inheritance | React
Mixins Considered Harmful | React
HOCの基本
JavaScriptには、関数を引数や戻り値として扱う「高階関数 (Higher-Order Function)」というものがあります。
HOCはコンポーネントを引数や戻り値として扱うので、「高階コンポーネント (Higher-Order Component)」と呼ばれます。
React-Reduxに登場するconnect関数などがこのHOCに該当します。
const hocHoge = (WrappedComponent) => {
// WrappedComponentに機能を付加したComponentを生成して返す
return class extends React.Component {
constructor(props) {
// 必要な情報を親から受け取ったり独自に持ったり...
super(props);
this.state = {
list: []
};
}
// 付加したい機能
anyFunction() {
const settings = this.props.settings;
// ...なんやかやデータ操作して更新
this.setState({
list: newList
});
}
render() {
// 渡す必要のないpropsは中抜きする
const { settings, ...otherProps } = this.props;
// anyFunctionの成果物と、残りのpropsを渡す
return (
<WrappedComponent list={this.state.list} {...otherProps} />
);
}
}
};
// ChildComponentを渡して、機能を追加された新たなコンポーネントを取得する
const EnhancedComponent = hocHoge(function ChildComponent({ list, text }) {
// listの出どころなんて知らない
return (
<ul>
{list.map((value, index) => (
<li key={index}>
{value}
<span>{text}</span>
</li>
))}
</ul>
);
});
// EnhancedComponentを利用する
function ParentComponent() {
// settingsの行く末なんて知らない
return (
<div>
<EnhancedComponent
settings={anyData}
text={anyText}
/>
</div>
);
}
hocHoge()
関数にChildComponent
を渡すと、機能が付加されたEnhancedComponent
を取得できます。
ParentComponent
は付加機能に必要なデータsettings
とChildComponent
が直接必要としているデータtext
を、区別無く渡しています。
ChildComponent
は、付加機能で生成されたデータlist
とtext
を区別無くprops
の一つとして受け取っています。
途中の処理は隠蔽されており、双方共に付加機能に対して特別な注意を払う必要はありません。
関心事の分離
公式サイトによると、HOCは横断的関心事のために使う(Use HOCs For Cross-Cutting Concerns)とあります。
横断的関心事とは、ビジネスロジックなどに基づいて分割した複数のモジュールが共通して"関心"(何をやりたいのか)を持っている事柄を指します。
例えば、データAを取得して表示するコンポーネントと、データBを取得して表示するコンポーネント、必要とするデータも描画する内容も異なりますが、WebAPIを利用してデータを取得したいという部分においては関心事が一致しています。
この関心事が横断的関心事です。
そしてこの関心事をうまい具合に切り出して(関心の分離)共有化する方法がHOCです。
↓横断的関心事についてはこちらの説明が分かりやすかったです。
アスペクト指向の基礎とさまざまな実装 | ITmedia エンタープライズ
連続クリックをまとめる機能を提供するHOC
隠蔽した処理の結果を渡すのではなくメソッドを渡していますし、ライフサイクルメソッドも利用していないので、HOCの使いどころを間違えているのかな?と一瞬悩みました。
しかし特定の機能の提供であり、コンポーネントの組み合わせ(Composition)とは異なりますし、ミックスインの代替案がHOCということならばHOCかなということで、HOCにしました。
(メソッドを渡すこと自体は公式サイトのサンプルにも事例があります。)
そういう時はこうすると良いよ的な情報がありましたら、コメントで頂けると幸いです。
/**
* 複数のクリックを一つにまとめるHOC
* @param {ReactComponent} WrappedComponent ラップされるコンポーネント
* @return {ReactComponent} ラップ後コンポーネント
*/
const joinClickHandler = (WrappedComponent) => {
// ラップするコンポーネントの定義
class JoinClickHandler extends React.Component {
constructor(props) {
super(props);
this.timer = null;
}
/**
* クリックハンドラ
* @param {Object} e eventオブジェクト
* @param {Function} callBack コールバック関数
*/
handleClick(e, callBack) {
e.persist(); //eventオブジェクトを切り離す
clearTimeout(this.timer);
this.timer = setTimeout(callBack, 200, e);
}
// ラップされるコンポーネントに、引き継いだpropsとハンドラを渡す
render() {
return (
<WrappedComponent
{...this.props}
handleClick={this.handleClick.bind(this)}
/>
);
}
}
// DevTool上で確認する際の名前を定義
JoinClickHandler.displayName = `JoinClickHandler(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
// ラップしたコンポーネントを返す
return JoinClickHandler;
};
/**
* joinClickHandlerを利用したボタン
*/
const JoinClickButton = joinClickHandler(function JoinClickButton({ handleClick }) {
const callBack = (e) => {
console.log(e.target.textContent);
};
return (
<button onClick={e => handleClick(e, callBack)}>
JoinClick
</button>
);
});
200ミリ秒以内のクリックが続いた場合、最後の1回にだけ反応します。
e.persist()
は、Reactのイベントオブジェクトを非同期処理に渡す際に必要な処理です。
このイベントオブジェクトは基本的に使いまわされており、イベントハンドラが終われば各プロパティは無効化されてしまいます。
非同期処理に渡す場合は、事前にe.persist()
を実行して切り離す必要があります。
もちろんpreventDefault()
やstopPropagation()
は、persist()
の前に実行する必要があります。
利用する側では、名前付きFunctionコンポーネントを定義してjoinClickHandler()
関数に引き渡しています。
handleClick
メソッドはprops
の一つとして、親コンポーネントからもらうその他のprops
と一緒に受け取ります。
SyntheticEvent > Event Pooling | React
React標準ハンドラとの比較サンプル
ReactのonClick
をそのまま利用した場合との挙動の違いを確認するサンプルコードです。
さらに、上記のjoinClickHandler
を拡張して、対応するクリック回数を指定できるHOCmultiClickHandler
も作ってみました。(実際に使う機会はなさそう…)
See the Pen JoinClickComponent (ReactHOC) by Bo_bee (@bo_bee) on CodePen.
参考情報
React
Handling Events | React
Double Click and Click on ReactJS Component | Stack Overflow
Reactでシングルクリックとダブルクリックを区別して使いたい
Higher-Order Component(HOC)の使い方と使用上の注意点
トリプルより後はなんて呼ぶのだろうと思って調べたところ、過去に一度も聞いた覚えの無い名前が並んでいました。
要素の数 | 特殊名 | 英名 |
---|---|---|
1 | シングル | single |
2 | ダブル | double |
3 | トリプル | triple |
4 | クオドループル | quadruple |
5 | クインチュープル | quintuple |
6 | セクスチュープル | sextuple |
7 | セプチュプル | septuple |
8 | オクチュプル | octuple |
9 | ノニュプル | nonuple |
10 | デキュプル | decuple |
100 | センチュプル | centuple |
from:タプル | Wikipedia