ReactJS两个组件进行通信

我刚刚开始使用ReactJS,有点卡住了我有一个问题。

我的应用程序本质上是一个带有filter和一个button来改变布局的列表。 目前我使用三个组件: <list />< Filters /><TopBar /> ,现在显然当我改变< Filters />设置时,我想触发<list />某个方法来更新我的视图。

我怎样才能让这三个组件互相交互,还是我需要某种forms的全局数据模型,我只需要对其进行更改呢?

最好的方法将取决于你打算如何安排这些组件。 现在想起几个例子:

  1. <Filters /><List />的子组件
  2. <Filters /><List />都是父组件的子代
  3. 完全存在于不同的根组件中的<Filters /><List />

可能还有其他的情况,我没有想到。 如果你的不适合这些,那么让我知道。 以下是我如何处理前两种情况的一些非常粗略的例子:

情景#1

您可以将<List />处理程序传递给<Filters /> ,然后可以在onChange事件上调用该处理程序以使用当前值过滤列表。

JSFiddle for#1→

 /** @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 />

JSFiddle for#2→

 /** @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

当组件之间不能进行任何forms的父子关系时, 文档build议build立一个全球事件系统 。

2closures组件:父/子

家长/孩子

如果树中的2个组件足够近,则可以简单地使用其他答案中已经描述的函数


门户

当你想让两个组件紧靠在一起以使它们与简单的函数(如普通的父/子)通信,但是你不希望这两个组件在DOM中具有父/子关系时使用门户,因为它暗示的visual / CSS约束(像z-index,opacity …)。

在这种情况下,您可以使用“门户”。 有使用门户不同的反应库,通常用于模式 ,popup窗口,工具提示…

考虑以下几点:

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

reactAppContainer呈现时,可能会产生以下DOM:

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

更多细节在这里


活动巴士

正如React 文档所述 :

对于没有父子关系的两个组件之间的通信,可以设置自己的全局事件系统。 订阅componentDidMount()中的事件,取消订阅componentWillUnmount(),并在收到事件时调用setState()。

有很多事情可以用来设置一个事件总线。 您可以创build一个侦听器数组,并且在事件发布时,所有侦听器都会收到该事件。 或者你可以使用像EventEmitter或PostalJs的东西


助焊剂

Flux基本上是一个事件总线,除了事件接收器是商店。 这与基本的事件总线系统类似,除了在React之外pipe理状态

原始的Flux实现看起来像是试图以hacky的方式进行Event-sourcing。

Redux对我来说是最接近事件采购的Flux实施,这有利于许多事件采购优势,如时间旅行的能力。 它与React没有严格关联,也可以与其他function视图库一起使用。

Egghead的Redux video教程是非常好的,并解释它如何在内部工作(这确实很简单)。


游标

游标来自ClojureScript / Om ,广泛用于React项目。 它们允许pipe理React之外的状态,并让多个组件对状态的相同部分具有读/写访问权限,而无需了解任何有关组件树的信息。

存在许多实现,包括ImmutableJS , React-cursors和Omniscient

编辑2016年 :似乎人们同意游标工作正常的小型应用程序,但它不能很好地扩展在复杂的应用程序。 Om Next不再有游标了(虽然Om是最初引入这个概念的)


榆树build筑学

Elm架构是Elm语言提出的架构。 即使Elm不是ReactJS,Elm架构也可以在React中完成。

Redux的作者Dan Abramov用React 实现了Elm架构。

Redux和Elm都非常出色,并且倾向于在前端增强事件采购概念,允许时间旅行debugging,撤消/重做,重播…

Redux和Elm之间的主要区别在于,Elm对于国家pipe理的要求往往更为严格。 在Elm中,您不能拥有本地组件状态或挂载/卸载挂接,并且所有DOM更改都必须由全局状态更改触发。 Elm体系结构提出了一种可扩展的方法,允许在单个不可变对象内处理所有状态,而Redux提出了一种方法来邀请您处理单个不可变对象中的大部分状态。

尽pipeElm的概念模型非常优雅,并且架构允许在大型应用程序上很好地扩展,但实际上可能很困难,或者需要更多的样板来实现简单的任务,例如在安装后着重于input或与现有库集成与一个命令性的接口(即JQuery插件)。 相关问题 。

而且,Elm架构涉及更多的代码样板。 这不是写详细或复杂,但我认为榆树架构更适合静态types的语言。


FRP

像RxJS,BaconJS或Kefir这样的库可以用来生成FRPstream来处理组件之间的通信。

你可以尝试例如Rx-React

我认为使用这些库与使用ELM语言提供的信号非常相似。

CycleJS框架不使用ReactJS,但使用vdom 。 它与Elm架构有许多相似之处(但在现实生活中更容易使用,因为它允许使用vdom挂钩),它广泛地使用RxJ而不是函数,如果您想要使用FRP,它可能是一个很好的灵感来源反应。 CycleJs Eggheadvideo很好理解它是如何工作的。


CSP

CSP(Communicating Sequential Processes)目前很stream行(主要是因为Go / goroutines和core.async / ClojureScript),但是你也可以在JS-CSP中使用它们。

James Long做了一个video,解释了如何使用React。

传奇

一个传奇是来自DDD / EventSourcing / CQRS世界的后端概念,也被称为“stream程pipe理器”。 这个项目正在普及,主要是为了减less处理副作用(即API调用等)的代价。 目前大多数人认为它只是服务于副作用,实际上更多的是关于解耦组件。

对于Flux体系结构(或者Redux)来说,这比完全新的通信系统更加值得称道,因为这个传说最后会发出Flux的动作。 这个想法是,如果你有widget1和widget2,并且你想让它们分离,你不能从widget1发起针对widget2的动作。 所以你只让widget1只发射自己定位的消息,而事件是一个监听widget1动作的“后台进程”,并且可能派发针对widget2的动作。 传奇是两个小工具之间的耦合点,但小工具仍然是分离的。

如果你有兴趣看看我的答案在这里


结论

如果您想查看使用这些不同样式的相同小应用程序的示例,请检查此存储库的分支。

我不知道从长远来看什么是最好的select,但我真的很喜欢Flux看起来像事件采购。

如果您不了解事件采购的概念,请看这个非常具有教育意义的博客: 用apache Samza将数据库翻出来 ,这是了解Flux为什么好的原因的必读书(但这也适用于FRP )

我认为社区同意,最有前途的Flux实现是Redux ,由于热重载,这将逐渐允许非常高效的开发者体验。 令人印象深刻的直播ala Bret Victor 发明原理video是可能的!

这是我处理这个的方式。
假设您有一个<select>的月份和一个<select>的date 。 天数取决于所选的月份。

这两个列表都属于第三个对象,即左侧面板。 两个<select>都是leftPanel <div>的子项
这是一个带有LeftPanel组件中的callback函数和处理程序的游戏

要testing它,只需将代码复制到两个分离的文件中,然后运行index.html 。 然后select一个月,看看如何改变天数。

dates.js

  /** @jsx React.DOM */ var monthsLength = [0,31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; var MONTHS_ARR = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; var DayNumber = React.createClass({ render: function() { return ( <option value={this.props.dayNum}>{this.props.dayNum}</option> ); } }); var DaysList = React.createClass({ getInitialState: function() { return {numOfDays: 30}; }, handleMonthUpdate: function(newMonthix) { this.state.numOfDays = monthsLength[newMonthix]; console.log("Setting days to " + monthsLength[newMonthix] + " month = " + newMonthix); this.forceUpdate(); }, handleDaySelection: function(evt) { this.props.dateHandler(evt.target.value); }, componentDidMount: function() { this.props.readyCallback(this.handleMonthUpdate) }, render: function() { var dayNodes = []; for (i = 1; i <= this.state.numOfDays; i++) { dayNodes = dayNodes.concat([<DayNumber dayNum={i} />]); } return ( <select id={this.props.id} onChange = {this.handleDaySelection}> <option value="" disabled defaultValue>Day</option> {dayNodes} </select> ); } }); var Month = React.createClass({ render: function() { return ( <option value={this.props.monthIx}>{this.props.month}</option> ); } }); var MonthsList = React.createClass({ handleUpdate: function(evt) { console.log("Local handler:" + this.props.id + " VAL= " + evt.target.value); this.props.dateHandler(evt.target.value); return false; }, render: function() { var monthIx = 0; var monthNodes = this.props.data.map(function (month) { monthIx++; return ( <Month month={month} monthIx={monthIx} /> ); }); return ( <select id = {this.props.id} onChange = {this.handleUpdate}> <option value="" disabled defaultValue>Month</option> {monthNodes} </select> ); } }); var LeftPanel = React.createClass({ dayRefresh: function(newMonth) { // Nothing - will be replaced }, daysReady: function(refreshCallback) { console.log("Regisering days list"); this.dayRefresh = refreshCallback; }, handleMonthChange: function(monthIx) { console.log("New month"); this.dayRefresh(monthIx); }, handleDayChange: function(dayIx) { console.log("New DAY: " + dayIx); }, render: function() { return( <div id="orderDetails"> <DaysList id="dayPicker" dateHandler={this.handleDayChange} readyCallback = {this.daysReady} /> <MonthsList data={MONTHS_ARR} id="monthPicker" dateHandler={this.handleMonthChange} /> </div> ); } }); React.renderComponent( <LeftPanel />, document.getElementById('leftPanel') ); 

还有用于运行左面板组件index.html的HTML

 <!DOCTYPE html> <html> <head> <title>Dates</title> <script src="ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script> <script src="//fb.me/react-0.11.1.js"></script> <script src="//fb.me/JSXTransformer-0.11.1.js"></script> </head> <style> #dayPicker { position: relative; top: 97px; left: 20px; width: 60px; height: 17px; } #monthPicker { position: relative; top: 97px; left: 22px; width: 95px; height: 17px; } select { font-size: 11px; } </style> <body> <div id="leftPanel"> </div> <script type="text/jsx" src="dates.js"></script> </body> </html> 

我看到这个问题已经被回答了,但是如果你想了解更多的细节, 组件之间总共有3种情况

  • 案例1:亲子沟通
  • 案例2:儿童之间的沟通
  • 情况3:不相关的组件(任何组件到任何组件)的通信

即使他们不是父母与子女的关系,也有这种可能性,那就是Flux。 对于那个叫做Alt.JS(带有Alt-Container)的实现来说,这是相当不错的(对我个人来说)。

例如,您可以使用依赖于组件详细信息中设置的边栏。 组件边栏与边栏function和边栏存储连接,而细节是DetailsActions和DetailsS​​tore。

你可以像那样使用AltContainer

 <AltContainer stores={{ SidebarStore: SidebarStore }}> <Sidebar/> </AltContainer> {this.props.content} 

哪些会保持商店(以及我可以使用“商店”,而不是“商店”道具)。 现在,{this.props.content}可以取决于路线。 可以说,/ Details将我们redirect到该视图。 细节将有例如一个checkbox,如果将被检查,会将侧边栏元素从X更改为Y.

从技术上来说,他们之间没有任何关系,要做到这一点很困难。 但是这是相当容易的。

现在我们来看DetailsActions。 我们将在那里创build

 class SiteActions { constructor() { this.generateActions( 'setSiteComponentStore' ); } setSiteComponent(value) { this.dispatch({value: value}); } } 

和DetailsS​​tore

 class SiteStore { constructor() { this.siteComponents = { Prop: true }; this.bindListeners({ setSiteComponent: SidebarActions.COMPONENT_STATUS_CHANGED }) } setSiteComponent(data) { this.siteComponents.Prop = data.value; } } 

而现在,这是魔法开始的地方。

正如你可以看到有bindListener到SidebarActions.ComponentStatusChanged将被使用,如果setSiteComponent将被使用。

现在在SidebarActions

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

我们有这样的事情。 它会调用该对象。 它会被调用,如果setSiteComponent存储将被使用(你可以在组件中使用例如onChange on Button ot什么)

现在在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。 这是什么意思? 或多或less意味着此方法需要等待DetailsS​​tore更新才能更新自身。

tl; Dr One Store正在监听商店中的方法,并会触发组件操作中的操作,这会更新自己的商店。

我希望它能以某种方式帮助你。

如果你想探索组件之间的通信select,并觉得它越来越难,那么你可以考虑采用一个好的devise模式: Flux 。

它只是定义如何存储和改变应用程序状态的规则集合,并使用该状态来呈现组件。

Flux有很多实现, Facebook的官方实现就是其中之一。 尽pipe它被认为是包含大多数样板代码的一个,但是由于大部分内容都是明确的,所以它更容易理解。

一些其他的替代品是flummox fluxxor fluxible和redux 。

当场景是组件不能在任何types的父子关系之间进行通信时,扩展@MichaelLaCroix的答案,文档build议设置全局事件系统。

如果<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() { } } 

我曾经是你现在所处的位置,作为一个初学者,你有时会觉得不适应这种反应方式。 我现在要试着用同样的方式来解决这个问题。

国家是沟通的基石

通常情况下,如果您指出了三个组件,那么您在此组件中更改状态的方式就是如此。

<List /> :这可能会显示一个项目的列表,取决于一个filter<Filters /> :filter选项,将改变你的数据。 <TopBar /> :选项列表。

为了协调所有这些交互,你需要一个更高的组件,我们把它称为App,它将把动作和数据传递给这个组件的每一个,所以例如可以看起来像这样

 <div> <List items={this.state.filteredItems}/> <Filter filter={this.state.filter} setFilter={setFilter}/> </div> 

所以当调用setFilter时,它将影响filteredItem并重新渲染这两个组件。 如果这不是完全清楚,我给你一个checkbox的例子,你可以检查一个单一的文件:

 import React, {Component} from 'react'; import {render} from 'react-dom'; const Person = ({person, setForDelete}) => ( <div> <input type="checkbox" name="person" checked={person.checked} onChange={setForDelete.bind(this, person)} /> {person.name} </div> ); class PeopleList extends Component { render() { return( <div> {this.props.people.map((person, i) => { return <Person key={i} person={person} setForDelete={this.props.setForDelete} />; })} <div onClick={this.props.deleteRecords}>Delete Selected Records</div> </div> ); } } // end class class App extends React.Component { constructor(props) { super(props) this.state = {people:[{id:1, name:'Cesar', checked:false},{id:2, name:'Jose', checked:false},{id:3, name:'Marbel', checked:false}]} } deleteRecords() { const people = this.state.people.filter(p => !p.checked); this.setState({people}); } setForDelete(person) { const checked = !person.checked; const people = this.state.people.map((p)=>{ if(p.id === person.id) return {name:person.name, checked}; return p; }); this.setState({people}); } render () { return <PeopleList people={this.state.people} deleteRecords={this.deleteRecords.bind(this)} setForDelete={this.setForDelete.bind(this)}/>; } } render(<App/>, document.getElementById('app')); 

下面的代码可以帮助我设置两个兄弟之间的通信。 这个设置是在render()和componentDidMount()调用期间在它们的父级完成的。 它基于https://reactjs.org/docs/refs-and-the-dom.html希望它有帮助。;

 class App extends React.Component<IAppProps, IAppState> { private _navigationPanel: NavigationPanel; private _mapPanel: MapPanel; constructor() { super(); this.state = {}; } // `componentDidMount()` is called by ReactJS after `render()` componentDidMount() { // Pass _mapPanel to _navigationPanel // It will allow _navigationPanel to call _mapPanel directly this._navigationPanel.setMapPanel(this._mapPanel); } render() { return ( <div id="appDiv" style={divStyle}> // `ref=` helps to get reference to a child during rendering <NavigationPanel ref={(child) => { this._navigationPanel = child; }} /> <MapPanel ref={(child) => { this._mapPanel = child; }} /> </div> ); } }