LoginSignup
14
12

More than 5 years have passed since last update.

【React】ダブルクリックによる処理の重複を防ぐ機能を提供するHOC

Last updated at Posted at 2019-04-14

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に該当します。

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} />
      );
    }
  }
};
HOCの利用
// 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は付加機能に必要なデータsettingsChildComponentが直接必要としているデータtextを、区別無く渡しています。
ChildComponentは、付加機能で生成されたデータlisttextを区別無くpropsの一つとして受け取っています。
途中の処理は隠蔽されており、双方共に付加機能に対して特別な注意を払う必要はありません。

関心事の分離

公式サイトによると、HOCは横断的関心事のために使う(Use HOCs For Cross-Cutting Concerns)とあります。
横断的関心事とは、ビジネスロジックなどに基づいて分割した複数のモジュールが共通して"関心"(何をやりたいのか)を持っている事柄を指します。
例えば、データAを取得して表示するコンポーネントと、データBを取得して表示するコンポーネント、必要とするデータも描画する内容も異なりますが、WebAPIを利用してデータを取得したいという部分においては関心事が一致しています。
この関心事が横断的関心事です。
そしてこの関心事をうまい具合に切り出して(関心の分離)共有化する方法がHOCです。

↓横断的関心事についてはこちらの説明が分かりやすかったです。
アスペクト指向の基礎とさまざまな実装 | ITmedia エンタープライズ

連続クリックをまとめる機能を提供するHOC

隠蔽した処理の結果を渡すのではなくメソッドを渡していますし、ライフサイクルメソッドも利用していないので、HOCの使いどころを間違えているのかな?と一瞬悩みました。
しかし特定の機能の提供であり、コンポーネントの組み合わせ(Composition)とは異なりますし、ミックスインの代替案がHOCということならば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;
};
HOCの利用
/**
 * 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

14
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
12