ReactJS两个组件通信

wou*_*_be 297 javascript reactjs

我刚刚开始使用ReactJS,而且我对我遇到的问题有点困惑.

我的应用程序本质上是一个带有过滤器的列表和一个更改布局的按钮.目前我使用的三个组成部分:<list />,< Filters /><TopBar />,现在很明显,当我更改设置的< Filters />我想在引发一些方法<list />来更新我的看法.

如何让这3个组件相互交互,或者我是否需要某种全局数据模型,我可以在其中进行更改?

Mic*_*oix 312

最好的方法取决于您计划如何安排这些组件.现在浮现在脑海中的一些示例场景:

  1. <Filters /> 是儿童的一部分 <List />
  2. 这两个<Filters /><List />是一个父组件的孩子
  3. <Filters /><List />完全生活在单独的根组件中.

可能还有其他我不想的场景.如果你的不适合这些,请告诉我.以下是我如何处理前两个场景的一些非常粗略的例子:

场景#1

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

JSFiddle#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);
Run Code Online (Sandbox Code Playgroud)

场景#2

与方案#1类似,但父组件将是向下传递处理函数的组件<Filters />,并将过滤后的列表传递给<List />.我喜欢这种方法更好,因为它解耦<List /><Filters />.

JSFiddle#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);
Run Code Online (Sandbox Code Playgroud)

场景#3

当组件无法在任何类型的父子关系之间进行通信时,文档建议设置全局事件系统.

  • 对于方案3,是否有推荐的方法?通过生成自定义合成事件的任何文档或示例?我没有在主要文档中找到任何内容. (28认同)
  • 场景 #3 链接已失效,现在重定向到不相关的 React 文档页面。 (15认同)
  • #2的好处在于它们只依赖于将prop传递给每个组件的父级:一个函数为`updateFilter`到`<Filters />`,一个数组为`items`到`<List />`.您可以在具有不同行为的其他父母中使用这些子组件,无论是一起还是独奏.例如,如果要显示动态列表但不需要过滤. (6认同)
  • 小提琴不起作用.有任何想法吗? (6认同)
  • @woutr_be不确定它是否符合您的要求,但在某个时候处于类似情况时,我们使用以下两个函数来对子组件和父组件之间的通信进行排序: - listenTo:function(eventName,eventCallback){$( window.document).bind(eventName,eventCallback);} triggerEvent:function(eventName,params){$ .event.trigger(eventName,params);}希望它有所帮助!(抱歉无法更好地格式化) (2认同)
  • 场景 #2 *使*很有意义...*直到*您必须危及设计(如果只​​是布局)——然后您意识到 EventHub/PubSub 的必要性。 (2认同)

Seb*_*ber 154

有多种方法可以使组件进行通信.有些可以适合您的用例.这是我发现有用的一些清单.

应对

家长/孩子直接沟通

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}/>
    )
  }
}
Run Code Online (Sandbox Code Playgroud)

这里子组件将使用一个值调用父提供的回调,并且父组件将能够获得父组中子组提供的值.

如果您构建应用程序的功能/页面,最好让单个父级管理回调/状态(也称为containersmart component),并且所有子级都是无状态的,只向父级报告内容.这样,您可以轻松地将父级的状态"共享"给需要它的任何孩子.


上下文

React 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>
);
Run Code Online (Sandbox Code Playgroud)

消费者正在使用渲染道具/儿童功能模式

查看此博客文章了解更多详情.

在React 16.3之前,我建议使用react-broadcast提供非常相似的API,并使用以前的上下文API.


门户

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

在这种情况下,您可以使用"门户".使用门户网站有不同的反应库,通常用于模态,弹出窗口,工具提示......

考虑以下:

<div className="a">
    a content
    <Portal target="body">
        <div className="b">
            b content
        </div>
    </Portal>
</div>
Run Code Online (Sandbox Code Playgroud)

在内部渲染时可以生成以下DOM reactAppContainer:

<body>
    <div id="reactAppContainer">
        <div className="a">
             a content
        </div>
    </div>
    <div className="b">
         b content
    </div>
</body>
Run Code Online (Sandbox Code Playgroud)

更多细节在这里


老虎机

您可以在某处定义一个插槽,然后从渲染树的另一个位置填充插槽.

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>
Run Code Online (Sandbox Code Playgroud)

这有点类似于门户网站,除了填充的内容将在您定义的插槽中呈现,而门户网站通常会呈现一个新的dom节点(通常是document.body的子节点)

检查react-slot-fill


活动巴士

如React 文档中所述:

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

您可以使用许多东西来设置事件总线.您只需创建一个侦听器数组,在事件发布时,所有侦听器都将接收该事件.或者你可以使用像EventEmitterPostalJs这样的东西


助焊剂

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

原始Flux实现看起来像是试图以hacky方式进行事件采购.

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

Egghead的Redux 视频教程非常好,并解释了它如何在内部工作(它确实很简单).


游标

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

存在许多实现,包括ImmutableJS,React-cursorsOmniscient

编辑2016:似乎人们同意游标适用于较小的应用程序,但它在复杂的应用程序上无法很好地扩展.Om Next不再使用游标(虽然最初引入概念的是Om)


榆树建筑

榆树架构是提出由使用的架构榆树语言.即使Elm不是ReactJS,Elm架构也可以在React中完成.

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

Redux和Elm都非常棒,并且倾向于在前端提供事件采购概念,包括时间旅行调试,撤销/重做,重播......

Redux和Elm之间的主要区别在于Elm对州管理往往更加严格.在Elm中,您不能拥有本地组件状态或mount/unmount挂钩,并且所有DOM更改必须由全局状态更改触发.Elm体系结构提出了一种可扩展的方法,允许在单个不可变对象内处理所有状态,而Redux提出了一种方法,它邀请您在单个不可变对象中处理状态的MOST.

虽然Elm的概念模型非常优雅,并且该体系结构允许在大型应用程序上很好地扩展,但实际上它可能很难或涉及更多样板来实现简单的任务,例如在安装后将焦点放在输入上,或者与现有库集成使用命令式界面(即JQuery插件).相关问题.

此外,Elm架构涉及更多代码样板.编写并不是那么冗长或复杂,但我认为Elm架构更适合静态类型语言.


FRP

像RxJS,BaconJS或Kefir这样的库可用于生成FRP流来处理组件之间的通信.

您可以尝试例如Rx-React

我认为使用这些库非常类似于使用ELM语言提供的信号.

CycleJS框架不使用ReactJS但使用vdom.它与Elm架构有许多相似之处(但在现实生活中更容易使用,因为它允许vdom挂钩)并且它广泛使用RxJ而不是功能,如果你想使用FRP,它可以成为一个很好的灵感来源反应.CycleJs Egghead视频很好理解它是如何工作的.


CSP

CSP(通信顺序进程)目前很流行(主要是因为Go/goroutines和core.async/ClojureScript),但你也可以在JS-CSP的 javascript中使用它们.

James Long做了一个视频,解释了它如何与React一起使用.

传奇

传奇是一种来自DDD/EventSourcing/CQRS世界的后端概念,也称为"流程管理器".它正在被redux-saga项目推广,主要是作为redux-thunk的替代品来处理副作用(即API调用等).目前大多数人认为它只是副作用的服务,但实际上更多的是解耦组件.

与全新的通信系统相比,它更像是对Flux架构(或Redux)的称赞,因为saga在最后发出了Flux动作.我们的想法是,如果您有widget1和widget2,并且希望它们分离,则无法从widget1触发针对widget2的操作.因此,您只使widget1触发针对其自身的操作,并且该saga是一个侦听widget1操作的"后台进程",并且可能会调度以widget2为目标的操作.传奇是两个小部件之间的耦合点,但小部件仍然是分离的.

如果您有兴趣,请在这里查看我的答案


结论

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

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

如果您不了解事件采购概念,请查看这个非常教学的博客:使用apache Samza将数据库内部翻出来,理解为什么Flux很好(但这也适用于FRP) )

我认为社区同意最有前途的Flux实现是Redux,由于热重新加载,它将逐步提供非常高效的开发人员体验.令人印象深刻的实时编码ala Bret Victor的发明原理视频是可能的!


Ali*_*eza 6

OK,有办法少做,但我仅仅想专注于使用采用存储终极版使您的生活更容易针对这些情况,而不是给你只有这种情况下的快速解决方案,采用纯反应最终会陷入困境中真正的大型应用程序以及组件之间的通信随着应用程序的增长而变得越来越难...

那么,Redux为您做什么呢?

Redux就像应用程序中的本地存储一样,可以在需要将数据用于应用程序中不同位置时使用。

基本上,Redux的想法最初是由变化产生的,但是进行了一些根本性的更改,包括通过仅创建一家商店来拥有一个真实来源的概念。

看下面的图,看看FluxRedux之间的一些区别...

Redux和Flux

如果您的应用程序需要组件之间的通信,请考虑从一开始就在应用程序中应用Redux

另外,从Redux文档中阅读这些单词可能对以下方面有所帮助:

随着对JavaScript单页应用程序的需求变得越来越复杂,我们的代码必须管理比以往更多的状态。此状态可以包括服务器响应和缓存的数据,以及尚未持久保存到服务器的本地创建的数据。UI状态的复杂性也在增加,因为我们需要管理活动路径,选定的选项卡,微调框,分页控件等。

管理这种不断变化的状态非常困难。如果一个模型可以更新另一个模型,那么一个视图可以更新一个模型,该模型可以更新另一个模型,这又可能导致另一个视图更新。在某些时候,您不再了解应用程序中发生的情况,因为您无法控制其状态的时间,原因和方式。当系统是不透明且不确定的系统时,很难重现错误或添加新功能。

好像还不够糟糕,请考虑一下新要求在前端产品开发中变得很普遍。作为开发人员,我们有望处理乐观更新,服务器端渲染,在执行路由转换之前获取数据等。我们发现自己试图管理一种从未有过的复杂性,而我们不可避免地会问一个问题:是时候放弃了吗?答案是不。

这种复杂性很难处理,因为我们混用了两个人类难以理解的概念:突变和异步性。我称他们为Mentos和可乐。两者之间的分隔可能很棒,但在一起却会造成混乱。像React这样的库试图通过消除异步和直接DOM操作来解决视图层中的这一问题。但是,管理数据状态由您自己决定。这是Redux进入的地方。

遵循Flux,CQRS和Event Sourcing步骤之后,Redux试图通过对更新的方式和时间施加一定的限制来使状态突变可预测。这些限制反映在Redux的三个原则中。


Sku*_*las 5

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

两个列表都由第三个对象(左侧面板)拥有.两个<select>也是leftPanel <div>的子节点.
这是一个带有回调和LeftPanel组件中的处理程序的游戏.

要测试它,只需将代码复制到两个单独的文件中并运行index.html.然后选择一个月,看看天数如何变化.

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')
    );
Run Code Online (Sandbox Code Playgroud)

以及用于运行左面板组件index.html的HTML

<!DOCTYPE html>
<html>
<head>
    <title>Dates</title>

    <script src="//cdnjs.cloudflare.com/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>
Run Code Online (Sandbox Code Playgroud)


Kal*_*sev 5

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

  • 案例一:亲子沟通
  • 案例2:孩子与家长的沟通
  • 情况3:不相关的组件(任何组件到任何组件)通信