メインコンテンツへ移動

React コンポーネント間のロジック复用

無料2019-05-26#Front-End#react reuse logic between components#react逻辑复用#react cross-cutting concerns#react and AOP#render props and high order components

Mixin から話す……

はじめに

React では、コンポーネントはコード复用の主���なユニットで、組合せに基づくコンポーネント复用メカニズム は非常に優雅です。しかし、より細粒度のロジック(状態ロジック、動作ロジックなど)については、复用するのはそれほど簡単ではありません:

Components are the primary unit of code reuse in React, but it's not always obvious how to share the state or behavior that one component encapsulates to other components that need that same state.

Use HOCs For Cross-Cutting Concerns より抜粋)

状態ロジックを切り離して复用可能な関数やコンポーネントとして抽出するのは困難です:

However, we often can't break complex components down any further because the logic is stateful and can't be extracted to a function or another component.

なぜなら、これまで一貫して、シンプルで直接的なコンポーネント動作拡張方式が欠けていたからです:

React doesn't offer a way to "attach" reusable behavior to a component (for example, connecting it to a store).

It's hard to reuse stateful logic between components より抜粋)

ちょっと待って、HOC は拡張方式ではないですか、甚至 Mixin もありますよね?

厳密に言えば、Mixin、Render Props、HOC などの方案はすべて既存の(コンポーネントメカニズムの)ゲームルール下で探索された上位パターンに過ぎません:

To be clear, mixins is an escape hatch to work around reusability limitations in the system. It's not idiomatic React.

Proposal for porting React's Mixin APIs to a generic primitive より抜粋)

HOCs are not part of the React API, per se. They are a pattern that emerges from React's compositional nature.

Higher-Order Components より抜粋)

コンポーネント間のロジック复用の問題を根本から很好地に解決する方法はありませんでした……Hooks が舞台に登場するまで

P.S.Mixin は下層解決方案のように見えます(React は内部サポートを提供)が、実際にはmixin() ツール関数を内置しただけで、唯一の特殊な点は衝突処理戦略です:

A class can use multiple mixins, but no two mixins can define the same method. Two mixins can, however, implement the same lifecycle method. In this case, each implementation will be invoked one after another.

一。探索

コンポーネント級以下の細粒度ロジック(例えば 横断的関心事 を処理する)をさらに复用するために、様々な方案が探索されました:

大まかな過程は以下の通りです:

理論基礎方案欠陥
そのまま移植OOP 复用パターンを借用Mixinコンポーネントの複雑さが急上昇し、理解が困難
宣言型は命令型より優れ、組合せは継承より優れるHigher-Order Components, Render Props多重抽象により Wrapper Hell が発生
関数型思想を借用Hooks書き方の制限、学習コストなど

二.Mixin

Mixins allow code to be shared between multiple React components. They are pretty similar to mixins in Python or traits in PHP.

Mixin 方案の出現は OOP 直感に由来し、React 自体はいくらか関数型の味わいがありますが、ユーザーの習慣に迎合するために、早期には React.createClass() API のみを提供してコンポーネントを定義していました:

React is influenced by functional programming but it came into the field that was dominated by object-oriented libraries. It was hard for engineers both inside and outside of Facebook to give up on the patterns they were used to.

当然ながら、(クラス)継承が直感的な試みとなりました。JavaScript のプロトタイプベースの拡張モードでは、継承に類似した Mixin 方案が首选となりました:

// Mixin を定義
var Mixin1 = {
  getMessage: function() {
    return 'hello world';
  }
};
var Mixin2 = {
  componentDidMount: function() {
    console.log('Mixin2.componentDidMount()');
  }
};

// Mixin を使用して既存のコンポーネントを增强
var MyComponent = React.createClass({
  mixins: [Mixin1, Mixin2],
  render: function() {
    return <div>{this.getMessage()}</div>;
  }
});

(上古文書 react/docs/docs/mixins.md より抜粋)

Mixin は主にライフサイクルロジックと状態ロジックの复用問題を解決するために使用されます:

It tries to be smart and"merges"lifecycle hooks. If both the component and the several mixins it uses define the componentDidMount lifecycle hook, React will intelligently merge them so that each method will be called. Similarly, several mixins can contribute to the getInitialState result.

外部からコンポーネントのライフサイクルを拡張することを許可し、Flux などのパターンで特に重要です:

It's absolutely necessary that any component extension mechanism has the access to the component's lifecycle.

欠陥

しかし、多くの欠陥が存在します:

  • コンポーネントと Mixin の間に暗黙の依存関係が存在(Mixin は頻繁にコンポーネントの特定の方法に依存しますが、コンポーネントを定義する際にはこの依存関係が分かりません)

  • 複数の Mixin の間で衝突が発生する可能性があります(例えば同じstate フィールドを定義)

  • Mixin はより多くの状態を追加する傾向があり、これはアプリケーションの予測可能性を低下させ(The more state in your application, the harder it is to reason about it.)、複雑さが急増します

暗黙の依存関係により依存関係が不透明になり、維持コストと理解コストが急速に上昇します:

  • コンポーネントの動作を素早く理解するのが困難で、すべての依存 Mixin の拡張動作及其び間の相互影響を完全に理解する必要があります

  • コンポーネント自身の方法和state フィールドは簡単に削除・変更できず、Mixin がそれに依存しているかどうかを確定するのが困難だからです

  • Mixin も維持が困難で、Mixin ロジックは最後に平らに合併されるため、1 つの Mixin の入出力を搞清楚するのが困難です

間違いなく、これらの問題は致命的です

したがって、React v0.13.0 は Mixin を放棄 し(継承)、代わりに HOC(組合せ)に向かいました:

Idiomatic React reusable code should primarily be implemented in terms of composition and not inheritance.

(Mixin 方案が存在する問題を考慮しない)単に機能から見ると、Mixin も HOC と類似した拡張を完了できます。例えば:

var SetIntervalMixin = {
  componentWillMount: function() {
    this.intervals = [];
  },
  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },
  componentWillUnmount: function() {
    this.intervals.forEach(clearInterval);
  }
};

// React v15.5.0 以下の React.createClass と等価
var createReactClass = require('create-react-class');

var TickTock = createReactClass({
  mixins: [SetIntervalMixin], // Mixin を使用
  getInitialState: function() {
    return {seconds: 0};
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // Mixin の方法を呼び出す
  },
  tick: function() {
    this.setState({seconds: this.state.seconds + 1});
  },
  render: function() {
    return (
      <p>
        React has been running for {this.state.seconds} seconds.
      </p>
    );
  }
});

ReactDOM.render(
  <TickTock />,
  document.getElementById('example')
);

Mixins より抜粋)

P.S.[React v15.5.0] は正式にReact.createClass() API を廃棄し、create-react-class に移行し、内置 Mixin も共に歴史となりました。詳細は React v15.5.0 を参照

三.Higher-Order Components

Mixin の後、HOC が重任を担い、コンポーネント間のロジック复用の推奨方案となりました:

A higher-order component (HOC) is an advanced technique in React for reusing component logic.

しかし、HOC は新人ではありませんReact.createClass() 時代からすでに存在していました。HOC はコンポーネント組合せメカニズムの上に構築されており、完全に上位パターンで、特殊なサポートに依存しません

形式的には高階関数に類似し、コンポーネントを 1 層包んで動作を拡張します:

Concretely, A higher-order component is a function that takes a component and returns a new component.

例えば:

// 高階コンポーネントを定義
var Enhance = ComposedComponent => class extends Component {
  constructor() {
    this.state = { data: null };
  }
  componentDidMount() {
    this.setState({ data: 'Hello' });
  }
  render() {
    return <ComposedComponent {...this.props} data={this.state.data} />;
  }
};

class MyComponent {
  render() {
    if (!this.data) return <div>Waiting...</div>;
    return <div>{this.data}</div>;
  }
}
// 高階コンポーネントを使用して普通コンポーネントを增强し、进而ロジック复用を実現
export default Enhance(MyComponent);

理論上、コンポーネントタイプパラメータを受け取りコンポーネントを返す関数はすべて高階コンポーネントです((Component, ...args) => Component)が、組合せを容易にするために、Component => Component 形式の HOC を推奨し、[偏関数適用](/articles/基礎構文-haskell ノート 1/#articleHeader11) を通じて他のパラメータを传入します。例えば:

// React Redux の`connect`
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

Mixin と比較

HOC モードでは、外層コンポーネントは Props を通じて内層コンポーネントの状態に影響を与え、直接その State を変更するのではなく:

Instead of managing the component's internal state, it wraps the component and passes some additional props to it.

さらに、复用可能な状態ロジックについては、この状態は状態を持つ高階コンポーネント中でのみ維持され(State を拡張することもコンポーネントスコープを持つことに相当)、衝突や相互干渉の問題は存在しません:

// この関数はコンポーネントを受け取り...
function withSubscription(WrappedComponent, selectData) {
  // ...そして別のコンポーネントを返す...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ...サブスクリプションを処理...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ...新鮮なデータでラップされたコンポーネントをレンダリング!
      // 追加の props も渡すことに注意
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

最も重要なのは、Mixin の平らにする + 合併 とは異なり、HOC は天然の階層構造(コンポーネントツリー構造)を持ち、この分解は複雑さを大幅に低減します

This way wrapper's lifecycle hooks work without any special merging behavior, by the virtue of simple component nesting!

欠陥

HOC はそれほど多くの致命的な問題はありませんが、いくつかの小さな欠陥も存在します:

  • 拡張性の制限:HOC は Mixin を完全に代替できません

  • Ref 伝達問題:Ref が遮断される

  • Wrapper Hell:HOC が氾濫し、Wrapper Hell が発生

拡張能力の制限

一部のシナリオでは、Mixin はできますが HOC はできません。例えば PureRenderMixin

PureRenderMixin implements shouldComponentUpdate, in which it compares the current props and state with the next ones and returns false if the equalities pass.

HOC は外部から子コンポーネントの State にアクセスできず、同時にshouldComponentUpdateを通じて不要な更新をフィルタリングできないためです。したがって、React は ES6 Class をサポートした後に React.PureComponent を提供してこの問題を解決しました

Ref 伝達問題

Ref の伝達問題は層層包装の下で非常に厄介で、関数 Ref は一部を緩和できます(HOC がノードの作成と破棄を知ることができるように)、そのため後にReact.forwardRef API ができました:

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // カスタム props"forwardedRef" を ref として割り当て
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // React.forwardRef によって提供される 2 番目のパラメータ"ref" に注意
  // これを通常の props ��して LogProps に渡すことができます。例えば"forwardedRef"
  // そしてそれを Component に接続できます
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

Forwarding refs in higher-order components より抜粋)

Wrapper Hell

1 層包んで解決できない問題はありません。あれば、2 層包めば……

Wrapper Hell 問題が直後に続きます:

You will likely find a"wrapper hell"of components surrounded by layers of providers, consumers, higher-order components, render props, and other abstractions.

多重抽象も複雑さと理解コストを増加させます。これが最も重要な欠陥で、HOC モードでは很好的な解決方法がありません

四.Render Props

HOC と同様に、Render Props もずっと存在してきた元老級パターンです:

The term"render prop"refers to a technique for sharing code between React components using a prop whose value is a function.

例えばカーソル位置関連のレンダリングロジックを抽出して复用し、Render Props パターンを通じて复用可能コンポーネントとターゲットコンポーネントを組合せます:

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the`render`prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

つまり、コンポーネントの一部のレンダリングロジックは外部から Props を通じて提供され、残りの変わらない部分は复用可能 です

HOC と比較

技術的に、二者はすべてコンポーネント組合せメカニズムに基づき、Render Props は HOC と同じ拡張能力を持ちます

Render Props と呼ばれますが、レンダリングロジックの复用にのみ使用できるわけではありません:

In fact, any prop that is a function that a component uses to know what to render is technically a"render prop".

Using Props Other Than render より抜粋)

このモードでは、コンポーネントはrender()を通じて組合せられることを示しており、HOC モードで Wrapper のrender()を通じて組合せ関係を確立するのと類似しています

形式的には、二者は非常に似ており、同様に 1 層の「Wrapper」を生成します(EComponentRP):

// HOC 定義
const HOC = Component => WrappedComponent;
// HOC 使用
const Component;
const EComponent = HOC(Component);
<EComponent />

// Render Props 定義
const RP = ComponentWithSpecialProps;
// Render Props 使用
const Component;
<RP specialRender={() => <Component />} />

さらに面白いのは、Render Props と HOC は相互に変換さえできます

function RP2HOC(RP) {
  return Component => {
    return class extends React.Component {
      static displayName = "RP2HOC";
      render() {
        return (
          <RP
            specialRender={renderOptions => (
              <Component {...this.props} renderOptions={renderOptions} />
            )}
          />
        );
      }
    };
  };
}
// 用法
const HOC = RP2HOC(RP);
const EComponent = HOC(Component);

function HOC2RP(HOC) {
  const RP = class extends React.Component {
    static displayName = "HOC2RP";
    render() {
      return this.props.specialRender();
    }
  };
  return HOC(RP);
}
// 用法
const RP = HOC2RP(HOC);
<RP specialRender={() => <Component />} />

オンライン Demo:https://codesandbox.io/embed/hocandrenderprops-0v72k

P.S. ビュー内容は完全に同じですが、コンポーネントツリー構造は大きく異なります:

[caption id="attachment_1950" align="alignnone" width="625"]react hoc to render props react hoc to render props[/caption]

React DevTools を通じてhttps://0v72k.codesandbox.io/ を查看できます

五.Hooks

HOC、Render Props、コンポーネント組合せ、Ref 伝達……コード复用なぜこれほど複雑なのか?

根本的な理由は、細粒度コード复用はコンポーネント复用と一緒に束縛されるべきではない からです:

Components are more powerful, but they have to render some UI. This makes them inconvenient for sharing non-visual logic. This is how we end up with complex patterns like render props and higher-order components.

HOC、Render Props などのコンポーネント組合せに基づく方案は、复用したいロジックをまずコンポーネントにパッケージし、コンポーネント复用メカニズムを利用してロジック复用を実現することに相当します。当然ながらコンポーネント复用に制限され、したがって拡張能力が制限される、Ref が遮断される、Wrapper Hell……などの問題が発生します

では、シンプルで直接的なコード复用方式はあるでしょうか?

関数です。复用可能ロジックを関数に抽出するのが最も直接的で、コストが最低のコード复用方式であるはずです:

Functions seem to be a perfect mechanism for code reuse. Moving logic between functions takes the least amount of effort.

しかし、状態ロジックについては、仍然としていくつかの抽象パターン(例えば Observable)を通じてのみ复用を実現できます:

However, functions can't have local React state inside them. You can't extract behavior like"watch window size and update the state"or"animate a value over time"from a class component without restructuring your code or introducing an abstraction like Observables.

これが Hooks の思路です:関数を最小のコード复用ユニットとし、同時にいくつかのパターンを内置して状態ロジックの复用を簡素化します

例えば:

function MyResponsiveComponent() {
  const width = useWindowWidth(); // 私たちのカスタム Hook
  return (
    <p>Window width is {width}</p>
  );
}

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  });

  return width;
}

([Making Sense of React Hooks](https://medium.com/ @dan_abramov/making-sense-of-react-hooks-fdbde8803889) より抜粋。オンライン Demo ��https://codesandbox.io/embed/reac-conf-2018-dan-abramov-hooks-example-mess-around-o5zcu を参照)

宣言型状態ロジック(const width = useWindowWidth())、セマンティクスが非常に自然です

他の方案と比較

上記の他の方案と比較して、Hooks はコンポーネント内ロジック复用がコンポーネント复用と一緒に束縛されないようにし、本当に下層から(コンポーネント間の)細粒度ロジックの复用問題を解決しようと試みています

さらに、この宣言型ロジック复用方案は、コンポーネント間の明示的データフローと組合せ思想をさらにコンポーネント内に延伸 し、React の理念に契合します:

Hooks apply the React philosophy (explicit data flow and composition) inside a component, rather than just between the components.

欠陥

Hooks も完璧ではなく、現時点では、その欠点は以下の通りです:

  • 追加の学習コスト(Functional Component と Class Component の間の困惑)

  • 書き方に制限がある(条件、ループ中に出現できない)、かつ書き方の制限はリファクタリングコストを増加

  • PureComponentReact.memo の浅比較のパフォーマンス最適化効果を破壊(最新のpropsstate を取得するため、毎回render() ごとにイベント処理関数を再作成する必要)

  • クロージャシーンで古いstateprops 値を参照する可能性

  • 内部実装が直観的ではない(可変のグローバル状態に依存し、それほど「純粋」ではなくなる)

  • React.memoshouldComponentUpdate を完全に代替できない(state change が取得できないため、props change のみ针对)

  • useState API 設計があまり完璧ではない

Drawbacks より抜粋)

参考資料

コメント

コメントはまだありません

コメントを書く