356

私は ReactJS を使い始めたばかりで、私が抱えている問題に少し固執しています。

私のアプリケーションは基本的に、フィルターとレイアウトを変更するためのボタンを備えたリストです。現時点では<list />、 、 、< Filters />の3 つのコンポーネントを使用しています。設定を変更したときに、ビューを更新するために何らかのメソッドをトリガーしたい<TopBar />のは明らかです。< Filters /><list />

これらの 3 つのコンポーネントを相互にやり取りさせるにはどうすればよいですか? または、変更を加えるだけの何らかのグローバル データ モデルが必要ですか?

4

11 に答える 11

339

最適なアプローチは、これらのコンポーネントをどのように配置する予定かによって異なります。今思いつくいくつかのシナリオの例:

  1. <Filters />の子コンポーネントです<List />
  2. <Filters />との両方<List />が親コンポーネントの子です
  3. <Filters />完全に<List />別々のルートコンポーネントに存在します。

私が考えていない他のシナリオがあるかもしれません。これらに当てはまらない場合は、お知らせください。最初の 2 つのシナリオをどのように処理してきたかについて、非常に大まかな例を次に示します。

シナリオ #1

ハンドラーを from から に渡すことができます<List />。これをイベント<Filters />で呼び出してonChange、現在の値でリストをフィルター処理できます。

#1 の JSFiddle →</a>

/** @jsx React.DOM */

var Filters = React.createClass({
  handleFilterChange: function() {
    var value = this.refs.filterInput.getDOMNode().value;
    this.props.updateFilter(value);
  },
  render: function() {
    return <input type="text" ref="filterInput" onChange={this.handleFilterChange} placeholder="Filter" />;
  }
});

var List = React.createClass({
  getInitialState: function() {
    return {
      listItems: ['Chicago', 'New York', 'Tokyo', 'London', 'San Francisco', 'Amsterdam', 'Hong Kong'],
      nameFilter: ''
    };
  },
  handleFilterUpdate: function(filterValue) {
    this.setState({
      nameFilter: filterValue
    });
  },
  render: function() {
    var displayedItems = this.state.listItems.filter(function(item) {
      var match = item.toLowerCase().indexOf(this.state.nameFilter.toLowerCase());
      return (match !== -1);
    }.bind(this));

    var content;
    if (displayedItems.length > 0) {
      var items = displayedItems.map(function(item) {
        return <li>{item}</li>;
      });
      content = <ul>{items}</ul>
    } else {
      content = <p>No items matching this filter</p>;
    }

    return (
      <div>
        <Filters updateFilter={this.handleFilterUpdate} />
        <h4>Results</h4>
        {content}
      </div>
    );
  }
});

React.renderComponent(<List />, document.body);

シナリオ 2

シナリオ 1 と似ていますが、親コンポーネントは にハンドラ関数を<Filters />渡し、フィルタリングされたリストを に渡し<List />ます。<List />からを分離するので、この方法の方が気に入ってい<Filters />ます。

#2 の JSFiddle →</a>

/** @jsx React.DOM */

var Filters = React.createClass({
  handleFilterChange: function() {
    var value = this.refs.filterInput.getDOMNode().value;
    this.props.updateFilter(value);
  },
  render: function() {
    return <input type="text" ref="filterInput" onChange={this.handleFilterChange} placeholder="Filter" />;
  }
});

var List = React.createClass({
  render: function() {
    var content;
    if (this.props.items.length > 0) {
      var items = this.props.items.map(function(item) {
        return <li>{item}</li>;
      });
      content = <ul>{items}</ul>
    } else {
      content = <p>No items matching this filter</p>;
    }
    return (
      <div className="results">
        <h4>Results</h4>
        {content}
      </div>
    );
  }
});

var ListContainer = React.createClass({
  getInitialState: function() {
    return {
      listItems: ['Chicago', 'New York', 'Tokyo', 'London', 'San Francisco', 'Amsterdam', 'Hong Kong'],
      nameFilter: ''
    };
  },
  handleFilterUpdate: function(filterValue) {
    this.setState({
      nameFilter: filterValue
    });
  },
  render: function() {
    var displayedItems = this.state.listItems.filter(function(item) {
      var match = item.toLowerCase().indexOf(this.state.nameFilter.toLowerCase());
      return (match !== -1);
    }.bind(this));

    return (
      <div>
        <Filters updateFilter={this.handleFilterUpdate} />
        <List items={displayedItems} />
      </div>
    );
  }
});

React.renderComponent(<ListContainer />, document.body);

シナリオ #3

コンポーネントがあらゆる種類の親子関係間で通信できない場合、ドキュメントではグローバル イベント システムを設定することを推奨しています

于 2014-01-24T00:54:29.790 に答える
207

コンポーネントを通信させる方法は複数あります。ユースケースに適したものもあります。ここに、私が知っていると役立つと思ったもののリストを示します。

反応する

親子ダイレクトコミュニケーション

const Child = ({fromChildToParentCallback}) => (
  <div onClick={() => fromChildToParentCallback(42)}>
    Click me
  </div>
);

class Parent extends React.Component {
  receiveChildValue = (value) => {
    console.log("Parent received value from child: " + value); // value is 42
  };
  render() {
    return (
      <Child fromChildToParentCallback={this.receiveChildValue}/>
    )
  }
}

ここで、子コンポーネントは親によって提供されたコールバックを値とともに呼び出し、親は親の子によって提供された値を取得できます。

アプリの機能/ページを構築する場合は、コールバック/状態 (containerまたはとも呼ばれますsmart component) を管理する単一の親を持ち、すべての子をステートレスにして、親にのみ報告することをお勧めします。このようにして、親の状態をそれを必要とする子に簡単に「共有」できます。


環境

React Context を使用すると、コンポーネント階層のルートで状態を保持し、この状態を非常に深くネストされたコンポーネントに簡単に挿入できます。小道具をすべての中間コンポーネントに渡す必要はありません。

これまで context は実験的な機能でしたが、React 16.3 で新しい API が利用可能になりました。

const AppContext = React.createContext(null)

class App extends React.Component {
  render() {
    return (
      <AppContext.Provider value={{language: "en",userId: 42}}>
        <div>
          ...
          <SomeDeeplyNestedComponent/>
          ...
        </div>
      </AppContext.Provider>
    )
  }
};

const SomeDeeplyNestedComponent = () => (
  <AppContext.Consumer>
    {({language}) => <div>App language is currently {language}</div>}
  </AppContext.Consumer>
);

コンシューマーはrender prop / children 関数パターンを使用しています

詳細については、このブログ投稿を確認してください。

React 16.3 より前は、非常によく似た API を提供する react-broadcastを使用し、以前のコンテキスト API を使用することをお勧めします。


ポータル

通常の親子関係のように、2 つのコンポーネントを近づけて単純な関数と通信させたい場合にポータルを使用します。それが意味する視覚的/CSSの制約(z-index、不透明度など)。

この場合、「ポータル」を使用できます。ポータルを使用するさまざまな反応ライブラリがあり、通常はモーダル、ポップアップ、ツールチップに使用されます...

次の点を考慮してください。

<div className="a">
    a content
    <Portal target="body">
        <div className="b">
            b content
        </div>
    </Portal>
</div>

内部でレンダリングすると、次の DOM を生成できますreactAppContainer

<body>
    <div id="reactAppContainer">
        <div className="a">
             a content
        </div>
    </div>
    <div className="b">
         b content
    </div>
</body>

詳細はこちら


スロット

どこかにスロットを定義してから、Render Tree の別の場所からスロットを埋めます。

import { Slot, Fill } from 'react-slot-fill';

const Toolbar = (props) =>
  <div>
    <Slot name="ToolbarContent" />
  </div>

export default Toolbar;

export const FillToolbar = ({children}) =>
  <Fill name="ToolbarContent">
    {children}
  </Fill>

これはポータルと少し似ていますが、塗りつぶされたコンテンツが定義したスロットにレンダリングされることを除けば、ポータルは通常、新しい dom ノード (多くの場合、document.body の子ノード) をレンダリングします。

react-slot-fillライブラリを確認する


イベントバス

Reactのドキュメントに記載されているように:

親子関係のない 2 つのコンポーネント間の通信のために、独自のグローバル イベント システムを設定できます。componentDidMount() でイベントをサブスクライブし、componentWillUnmount() でサブスクライブを解除し、イベントを受け取ったら setState() を呼び出します。

イベントバスのセットアップに使用できるものはたくさんあります。リスナーの配列を作成するだけで、イベントの発行時にすべてのリスナーがイベントを受け取ります。または、 EventEmitterPostalJsなどを使用できます


フラックス

Fluxは基本的にイベント バスですが、イベント レシーバーはストアです。これは、状態が React の外部で管理されることを除いて、基本的なイベント バス システムに似ています。

元の Flux 実装は、ハックな方法でイベント ソーシングを行う試みのように見えます。

Reduxは私にとって、イベント ソーシングに最も近い Flux 実装であり、タイム トラベル機能などのイベント ソーシングの利点の多くを利用します。React に厳密にリンクされているわけではなく、他の機能ビュー ライブラリでも使用できます。

Egghead の Reduxビデオ チュートリアルは非常に素晴らしく、内部でどのように機能するかを説明しています (非常にシンプルです)。


カーソル

カーソルはClojureScript/Omから来ており、React プロジェクトで広く使用されています。それらは、React の外部で状態を管理することを許可し、コンポーネント ツリーについて何も知る必要なく、状態の同じ部分への読み取り/書き込みアクセスを複数のコンポーネントに許可します。

ImmutableJSReact-cursorsOmniscientなど、多くの実装が存在します

Edit 2016 : 人々はカーソルが小さなアプリではうまく機能することに同意しているようですが、複雑なアプリではうまくスケーリングしません. Om Next にはもうカーソルがありません (最初にコンセプトを導入したのは Om ですが)。


Elm アーキテクチャ

Elm アーキテクチャは、 Elm 言語で使用するために提案されたアーキテクチャです。Elm が ReactJS でなくても、Elm アーキテクチャは React でも実行できます。

Redux の作成者である Dan Abramov は、React を使用して Elm アーキテクチャを実装しました。

Redux と Elm はどちらも非常に優れており、フロントエンドでイベント ソーシングの概念を強化する傾向があり、どちらもタイム トラベルのデバッグ、元に戻す/やり直し、再生を可能にします...

Redux と Elm の主な違いは、Elm は状態管理に関してより厳密になる傾向があることです。Elm では、ローカル コンポーネントの状態やマウント/アンマウント フックを使用することはできず、すべての DOM の変更はグローバルな状態の変更によってトリガーされる必要があります。Elm アーキテクチャは、単一の不変オブジェクト内のすべての状態を処理できるスケーラブルなアプローチを提案しますが、 Reduxは、単一の不変オブジェクトでほとんどの状態を処理するように勧めるアプローチを提案します。

Elm の概念モデルは非常に洗練されており、アーキテクチャは大規模なアプリにうまくスケーリングできますが、実際には、マウント後に入力にフォーカスを当てたり、既存のライブラリと統合したりするなどの単純なタスクを達成するのは困難であるか、より多くのボイラープレートを必要とする場合があります。命令型インターフェース (つまり、JQuery プラグイン) を使用します。関連する問題

また、Elm アーキテクチャには、より多くのコードボイラープレートが含まれます。それほど冗長でも複雑でもありませんが、Elm アーキテクチャは静的に型付けされた言語により適していると思います。


FRP

RxJS、BaconJS、Kefir などのライブラリを使用して FRP ストリームを生成し、コンポーネント間の通信を処理できます。

たとえば、Rx-Reactを試すことができます

これらのライブラリを使用することは、ELM 言語がシグナルで提供するものを使用することに非常に似ていると思います。

CycleJSフレームワークは ReactJS を使用せず、 vdom を使用します。Elm アーキテクチャと多くの類似点があります (ただし、vdom フックが許可されているため、実際にはより使いやすくなっています)。関数の代わりに RxJ を広く使用しているため、FRP を使用したい場合に良いインスピレーションの源になります。反応する。CycleJs Egghead の動画は、その仕組みを理解するのに役立ちます。


CSP

CSP (Communicating Sequential Processes) は現在人気があります (主に Go/goroutines と core.async/ClojureScript のため) が、JS-CSPを使用して JavaScript でも使用できます。

James Long は、React での使用方法を説明するビデオを作成しました。

サガ

サガは、DDD / EventSourcing / CQRS の世界に由来するバックエンドの概念であり、「プロセス マネージャー」とも呼ばれます。これはredux-sagaプロジェクトによって一般化されており、主に副作用 (API 呼び出しなど) を処理するための redux-thunk の代替として使用されています。ほとんどの人は現在、それが副作用のためだけに役立つと考えていますが、実際にはコンポーネントの分離に関するものです.

サガは最後に Flux アクションを発行するため、まったく新しい通信システムというよりは、Flux アーキテクチャ (または Redux) を補完するものです。アイデアは、widget1 と widget2 があり、それらを分離したい場合、widget1 から widget2 をターゲットとするアクションを起動できないということです。したがって、widget1 は自分自身を対象とするアクションのみを起動し、saga は widget1 のアクションをリッスンする「バックグラウンド プロセス」であり、widget2 を対象とするアクションをディスパッチする場合があります。サガは 2 つのウィジェット間の結合点ですが、ウィジェットは分離されたままです。

興味がある場合は、こちらの私の回答をご覧ください


結論

これらの異なるスタイルを使用した同じ小さなアプリの例を見たい場合は、このリポジトリのブランチを確認してください。

長期的にはどのオプションが最適かはわかりませんが、Flux がイベント ソーシングのように見える点がとても気に入っています。

イベント ソーシングの概念がわからない場合は、この非常に教育的なブログをご覧ください: Turning the database inside out with apache Samza 。Fluxが優れている理由を理解するために必読です (ただし、これは FRP にも当てはまる可能性があります)。 )

コミュニティは、最も有望な Flux 実装がReduxであることに同意していると思います。Reduxは、ホット リロードのおかげで非常に生産的な開発者エクスペリエンスを徐々に可能にします。Bret Victor のInventing on Principle ビデオの印象的なライブコーディングが可能です!

于 2015-07-22T12:48:28.467 に答える
4

質問はすでに回答されているようですが、詳細を知りたい場合は、コンポーネント間の通信の合計 3 つのケースがあります

  • ケース 1: 親から子への通信
  • ケース 2: 子から親へのコミュニケーション
  • ケース 3: 関連のないコンポーネント (任意のコンポーネントから任意のコンポーネント) の通信
于 2016-11-12T21:02:52.010 に答える
2

コンポーネントがあらゆる種類の親子関係間で通信できないというシナリオの場合、@MichaelLaCroix の回答を拡張し、ドキュメントではグローバル イベント システムの設定を推奨しています。

<Filters /><TopBar />上記の関係のいずれも持たない場合、単純なグローバル エミッタを次のように使用できます。

componentDidMount- イベントに申し込む

componentWillUnmount- イベントから退会する

React.js と EventSystem コード

EventSystem.js

class EventSystem{

    constructor() {
        this.queue = {};
        this.maxNamespaceSize = 50;
    }

    publish(/** namespace **/ /** arguments **/) {
        if(arguments.length < 1) {
            throw "Invalid namespace to publish";
        }

        var namespace = arguments[0];
        var queue = this.queue[namespace];

        if (typeof queue === 'undefined' || queue.length < 1) {
            console.log('did not find queue for %s', namespace);
            return false;
        }

        var valueArgs = Array.prototype.slice.call(arguments);

        valueArgs.shift(); // remove namespace value from value args

        queue.forEach(function(callback) {
            callback.apply(null, valueArgs);
        });

        return true;
    }

    subscribe(/** namespace **/ /** callback **/) {
        const namespace = arguments[0];
        if(!namespace) throw "Invalid namespace";
        const callback = arguments[arguments.length - 1];
        if(typeof callback !== 'function') throw "Invalid callback method";

        if (typeof this.queue[namespace] === 'undefined') {
            this.queue[namespace] = [];
        }

        const queue = this.queue[namespace];
        if(queue.length === this.maxNamespaceSize) {
            console.warn('Shifting first element in queue: `%s` since it reached max namespace queue count : %d', namespace, this.maxNamespaceSize);
            queue.shift();
        }

        // Check if this callback already exists for this namespace
        for(var i = 0; i < queue.length; i++) {
            if(queue[i] === callback) {
                throw ("The exact same callback exists on this namespace: " + namespace);
            }
        }

        this.queue[namespace].push(callback);

        return [namespace, callback];
    }

    unsubscribe(/** array or topic, method **/) {
        let namespace;
        let callback;
        if(arguments.length === 1) {
            let arg = arguments[0];
            if(!arg || !Array.isArray(arg)) throw "Unsubscribe argument must be an array";
            namespace = arg[0];
            callback = arg[1];
        }
        else if(arguments.length === 2) {
            namespace = arguments[0];
            callback = arguments[1];
        }

        if(!namespace || typeof callback !== 'function') throw "Namespace must exist or callback must be a function";
        const queue = this.queue[namespace];
        if(queue) {
            for(var i = 0; i < queue.length; i++) {
                if(queue[i] === callback) {
                    queue.splice(i, 1); // only unique callbacks can be pushed to same namespace queue
                    return;
                }
            }
        }
    }

    setNamespaceSize(size) {
        if(!this.isNumber(size)) throw "Queue size must be a number";
        this.maxNamespaceSize = size;
        return true;
    }

    isNumber(n) {
        return !isNaN(parseFloat(n)) && isFinite(n);
    }

}

NotificationComponent.js

class NotificationComponent extends React.Component {

    getInitialState() {
        return {
            // optional. see alternative below
            subscriber: null
        };
    }

    errorHandler() {
        const topic = arguments[0];
        const label = arguments[1];
        console.log('Topic %s label %s', topic, label);
    }

    componentDidMount() {
        var subscriber = EventSystem.subscribe('error.http', this.errorHandler);
        this.state.subscriber = subscriber;
    }

    componentWillUnmount() {
        EventSystem.unsubscribe('error.http', this.errorHandler);

        // alternatively
        // EventSystem.unsubscribe(this.state.subscriber);
    }

    render() {

    }
}
于 2016-11-08T06:25:59.240 に答える
0

親子関係でなくても、そういう可能性はある――それがフラックス。Alt.JS(Alt-Containerを使用)と呼ばれる(個人的には)かなり良い実装があります。

たとえば、コンポーネントの詳細で設定されている内容に依存するサイドバーを持つことができます。Component Sidebar は SidebarActions と SidebarStore に接続され、Details は DetailsActions と DetailsS​​tore に接続されます。

そのような AltContainer を使用できます

<AltContainer stores={{
                    SidebarStore: SidebarStore
                }}>
                    <Sidebar/>
</AltContainer>

{this.props.content}

これはストアを保持します(「ストア」小道具の代わりに「ストア」を使用できます)。さて、 {this.props.content} は、ルートに応じて詳細になる可能性があります。/Details がそのビューにリダイレクトするとしましょう。詳細には、たとえば、チェックされた場合にサイドバー要素を X から Y に変更するチェックボックスがあります。

技術的にはそれらの間に関係はなく、フラックスなしでは難しいでしょう。しかし、それはかなり簡単です。

それでは、DetailsActions に進みましょう。そこで作成いたします

class SiteActions {
constructor() {
    this.generateActions(
        'setSiteComponentStore'
    );
}

setSiteComponent(value) {
    this.dispatch({value: value});
}
}

と詳細ストア

class SiteStore {
constructor() {
    this.siteComponents = {
        Prop: true
    };

    this.bindListeners({
        setSiteComponent: SidebarActions.COMPONENT_STATUS_CHANGED
    })
}

setSiteComponent(data) {
    this.siteComponents.Prop = data.value;
}
}

そして今、ここから魔法が始まる。

ご覧のとおり、setSiteComponent が使用される場合に使用される SidebarActions.ComponentStatusChanged への bindListener があります。

SidebarActions に追加

    componentStatusChanged(value){
    this.dispatch({value: value});
}

私たちはそのようなものを持っています。呼び出し時にそのオブジェクトをディスパッチします。そして、ストア内のsetSiteComponentが使用される場合に呼び出されます(たとえば、ボタンなどのonChange中にコンポーネントで使用できます)

これで SidebarStore に

    constructor() {
    this.structures = [];

    this.bindListeners({
        componentStatusChanged: SidebarActions.COMPONENT_STATUS_CHANGED
    })
}

    componentStatusChanged(data) {
    this.waitFor(DetailsStore);

    _.findWhere(this.structures[0].elem, {title: 'Example'}).enabled = data.value;
}

ここで、DetailsS​​tore を待機することがわかります。どういう意味ですか?多かれ少なかれ、このメソッドは、自身を更新する前に DetailsS​​tore が更新されるのを待つ必要があることを意味します。

tl;dr One Store はストア内のメソッドをリッスンしており、独自のストアを更新するコンポーネント アクションからアクションをトリガーします。

何とかお役に立てれば幸いです。

于 2015-12-13T21:53:55.390 に答える