我刚开始使用 ReactJS,但对我遇到的一个问题有点卡住了。
我的应用程序本质上是一个带有过滤器的列表和一个用于更改布局的按钮。目前我正在使用三个组件:<list />,< Filters />和<TopBar />,现在很明显,当我更改 中的设置时,< Filters />我想触发一些方法<list />来更新我的视图。
我怎样才能让这 3 个组件相互交互,或者我是否需要某种全局数据模型,我可以在其中进行更改?
我刚开始使用 ReactJS,但对我遇到的一个问题有点卡住了。
我的应用程序本质上是一个带有过滤器的列表和一个用于更改布局的按钮。目前我正在使用三个组件:<list />,< Filters />和<TopBar />,现在很明显,当我更改 中的设置时,< Filters />我想触发一些方法<list />来更新我的视图。
我怎样才能让这 3 个组件相互交互,或者我是否需要某种全局数据模型,我可以在其中进行更改?
最佳方法取决于您计划如何安排这些组件。现在想到的一些示例场景:
<Filters /> 是一个子组件 <List /><Filters />和<List />是一个父组件的孩子<Filters />并<List />完全生活在单独的根组件中。可能还有其他我没有想到的场景。如果您的不适合这些,请告诉我。下面是一些非常粗略的例子,说明我是如何处理前两种情况的:
您可以从<List />to传递一个处理程序<Filters />,然后可以在onChange事件上调用该处理程序以使用当前值过滤列表。
/** @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);
与场景#1 类似,但父组件会将处理程序函数传递给<Filters />,并将过滤后的列表传递给<List />。我喜欢这种方法更好,因为它解耦<List />从<Filters />。
/** @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);
当组件无法在任何类型的父子关系之间进行通信时,文档建议设置一个全局事件系统。
有多种方法可以让组件进行通信。有些可能适合您的用例。这是我发现有用的一些列表。
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 允许在组件层次结构的根部保存状态,并且能够轻松地将此状态注入到非常深的嵌套组件中,而无需将 props 传递给每个中间组件。
到目前为止,上下文是一项实验性功能,但 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 之前,我建议使用react-broadcast提供非常相似的 API,并使用以前的上下文 API。
当您希望将 2 个组件靠近在一起以使它们与简单的功能进行通信时使用门户网站,例如在普通的父/子中,但您不希望这 2 个组件在 DOM 中具有父/子关系,因为它暗示的视觉/CSS 约束(如 z-index、opacity...)。
在这种情况下,您可以使用“门户”。有使用portals 的不同react库,通常用于模态、弹出窗口、工具提示......
考虑以下:
<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>
您在某处定义一个插槽,然后从渲染树的另一个位置填充该插槽。
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文档中所述:
对于没有父子关系的两个组件之间的通信,您可以设置自己的全局事件系统。在 componentDidMount() 中订阅事件,在 componentWillUnmount() 中取消订阅,并在收到事件时调用 setState()。
有很多东西可以用来设置事件总线。您可以只创建一个侦听器数组,并在事件发布时,所有侦听器都会收到该事件。或者你可以使用EventEmitter或PostalJs 之类的东西
Flux基本上是一个事件总线,除了事件接收器是存储。这类似于基本的事件总线系统,除了状态是在 React 之外管理的
最初的 Flux 实现看起来像是试图以一种骇人听闻的方式进行事件溯源。
Redux对我来说是最接近事件溯源的 Flux 实现,它有许多事件溯源的优势,比如时间旅行的能力。它没有严格链接到 React,也可以与其他功能视图库一起使用。
Egghead 的 Redux视频教程非常好,并解释了它的内部工作原理(它真的很简单)。
游标来自ClojureScript/Om并广泛用于 React 项目。它们允许在 React 之外管理状态,并让多个组件对状态的同一部分具有读/写访问权限,而无需了解有关组件树的任何信息。
存在许多实现,包括ImmutableJS、React-cursors和Omniscient
2016 年编辑:似乎人们同意游标适用于较小的应用程序,但它在复杂的应用程序上不能很好地扩展。Om Next 不再有游标(虽然最初是 Om 引入了这个概念)
在榆树架构是提出由使用的架构榆树语言。即使 Elm 不是 ReactJS,Elm 架构也可以在 React 中完成。
Redux 的作者 Dan Abramov使用 React实现了 Elm 架构。
Redux 和 Elm 都非常棒,并且倾向于在前端赋予事件溯源概念,两者都允许时间旅行调试、撤消/重做、重播……
Redux 和 Elm 之间的主要区别在于 Elm 往往对状态管理更加严格。在 Elm 中,您不能拥有本地组件状态或挂载/卸载挂钩,并且所有 DOM 更改都必须由全局状态更改触发。Elm 架构提出了一种可扩展的方法,允许处理单个不可变对象内的所有状态,而 Redux 提出一种方法,邀请您在单个不可变对象中处理大部分状态。
虽然 Elm 的概念模型非常优雅并且架构允许在大型应用程序上很好地扩展,但在实践中可能很难或涉及更多样板来实现简单的任务,例如在安装后将焦点放在输入上,或与现有库集成带有命令式接口(即 JQuery 插件)。相关问题。
此外,Elm 架构涉及更多代码样板。编写起来并不冗长或复杂,但我认为 Elm 架构更适合静态类型语言。
RxJS、BaconJS 或 Kefir 等库可用于生成 FRP 流以处理组件之间的通信。
您可以尝试例如Rx-React
我认为使用这些库与使用 ELM 语言提供的信号非常相似。
CycleJS框架不使用 ReactJS 而是使用vdom。它与 Elm 架构有很多相似之处(但在现实生活中更容易使用,因为它允许 vdom 挂钩)并且它广泛使用 RxJs 而不是函数,如果你想将 FRP 与react。CycleJs Egghead 视频很好地理解了它是如何工作的。
CSP(Communicating Sequential Processes)目前很流行(主要是因为 Go/goroutines 和 core.async/ClojureScript),但你也可以在JS-CSP 的javascript 中使用它们。
James Long 制作了一段视频,解释了如何将其与 React 一起使用。
saga 是来自 DDD / EventSourcing / CQRS 世界的后端概念,也称为“流程管理器”。它正在被redux-saga项目推广,主要是作为 redux-thunk 的替代品来处理副作用(即 API 调用等)。大多数人目前认为它只是为副作用服务,但它实际上更多是关于解耦组件。
它更像是对 Flux 架构(或 Redux)的赞美,而不是全新的通信系统,因为 saga 最后会发出 Flux 动作。这个想法是,如果您有小部件 1 和小部件 2,并且您希望它们解耦,则不能从小部件 1 触发针对小部件 2 的操作。因此,您让 widget1 只触发针对自身的操作,而 saga 是一个“后台进程”,它侦听 widget1 操作,并可能调度针对 widget2 的操作。传奇是两个小部件之间的耦合点,但小部件保持解耦。
如果你有兴趣看看我的答案在这里
如果您想查看使用这些不同样式的同一个小应用程序的示例,请查看此存储库的分支。
我不知道从长远来看最好的选择是什么,但我真的很喜欢 Flux 看起来像事件溯源。
如果您不了解事件溯源的概念,请查看这个非常有教育意义的博客:使用 apache Samza 将数据库翻过来,这是了解 Flux 为什么很好的必读之物(但这也适用于 FRP )
我认为社区同意最有前途的 Flux 实现是Redux,由于热重载,它将逐步提供非常高效的开发人员体验。令人印象深刻的现场编码 ala Bret Victor 的发明原理视频是可能的!
好的,有几种方法可以做到,但我只希望专注于使用Redux使用 store ,这使您在这些情况下的生活更轻松,而不是仅针对这种情况提供快速解决方案,使用纯 React 最终会陷入困境随着应用程序的增长,真正的大型应用程序和组件之间的通信变得越来越困难......
那么Redux为你做了什么?
Redux 就像应用程序中的本地存储,只要您需要在应用程序的不同位置使用数据,就可以使用它...
基本上,Redux 的想法最初来自于flux,但有一些根本性的变化,包括通过只创建一个商店来拥有一个真实来源的概念......
查看下图,了解Flux和Redux之间的一些差异......
如果您的应用程序需要组件之间的通信,请考虑从一开始就在您的应用程序中应用Redux ...
从 Redux 文档中阅读这些词可能有助于开始:
随着 JavaScript 单页应用程序的需求变得越来越复杂,我们的代码必须管理比以往更多的状态。此状态可以包括服务器响应和缓存数据,以及尚未保存到服务器的本地创建数据。UI 状态的复杂性也在增加,因为我们需要管理活动路由、选定的选项卡、微调器、分页控件等。
管理这种不断变化的状态很困难。如果一个模型可以更新另一个模型,那么一个视图可以更新一个模型,该模型更新另一个模型,这反过来可能会导致另一个视图更新。在某些时候,您不再了解应用程序中发生了什么,因为您无法控制其状态的时间、原因和方式。当系统不透明且不确定时,很难重现错误或添加新功能。
好像这还不够糟糕,请考虑新需求在前端产品开发中变得普遍。作为开发人员,我们需要处理乐观更新、服务器端渲染、在执行路由转换之前获取数据等。我们发现自己正试图管理我们以前从未处理过的复杂性,我们不可避免地会问一个问题:是时候放弃了吗?答案是不。
这种复杂性很难处理,因为我们混合了两个人类思维很难推理的概念:变异和异步。我称它们为曼妥思和可乐。两者分开都可以很好,但它们在一起会造成混乱。像 React 这样的库试图通过移除异步和直接 DOM 操作来解决视图层中的这个问题。但是,管理数据状态取决于您。这就是 Redux 进入的地方。
遵循Flux、CQRS 和事件溯源的步骤,Redux 尝试通过对更新发生的方式和时间施加某些限制来使状态变化可预测。这些限制体现在 Redux 的三个原则中。
这就是我处理这件事的方式。
假设您有一个 <select> 代表Month和一个 <select> 代表Day。天数取决于所选月份。
这两个列表都属于第三个对象,即左侧面板。<select> 也是 leftPanel <div> 的子项。
这是一个带有回调和LeftPanel组件中的处理程序的游戏。
要测试它,只需将代码复制到两个单独的文件中并运行index.html。然后选择一个月,看看天数如何变化。
日期.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="//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>
我看到问题已经回答了,但是如果你想了解更多细节,组件之间的通信总共有3种情况: