react-bootstrap 卸载父元素时 ModalTrigger 不会隐藏

react-bootstrap ModalTrigger doesn't hide when the parent element is unmounted

我们在将 react-bootstrap 的 ModalTrigger 与项目数组一起使用时遇到了一个奇怪的行为,因为当 parent/owner 项目被卸载时它不会消失。我们怀疑这与 React 的虚拟 DOM 和差异机制有关,and/or 我们自己对 ModalTrigger 的误用。

设置很简单:Content React 组件有一个状态,其中包含一个项目名称数组。它还具有一个 onClick(name) 函数,可通过 setState 从数组中删除该名称。在渲染中,它使用 _.map 创建一堆 Item 反应组件。

每个 Item 组件都显示其名称和一个包含标记为 "delete me" 的按钮的 ModalTrigger。单击按钮,它会打开模态;单击模态中的 "OK" 并执行对 Content remove 函数的回调。

当删除最后一个项目时,它工作正常:卸载最终的 Item 组件,以及 ModalTrigger 及其对应的模态。

我们看到的有问题的行为是在删除除最后一项以外的任何项目时。该项目已删除,但模态保持打开状态,而我天真地希望模态消失,因为父 ModalTrigger 消失了。不仅如此,再次点击"ok"时,列表中的下一项被移除,以此类推,直到模态恰好与最后一项相关联,此时点击"ok"最终将隐藏它。

我们的集体预感是,这是由于overlayMixin 的_overlayTarget 是文档中的匿名元素造成的,因此不同的ModalTriggers 无法区分它们。因此,当父级卸载并且 React 查找 DOM diff 时,它会看到前一个触发器并说 "hey, that could work".

整个问题可以通过在 Item 的内部 _onClick() 函数中添加一个 hide() 调用来轻松解决,正如代码中注释掉的那样,我们终于得出了我的问题:

我是否正确使用了 ModalTrigger,因为它期望它在父级卸载时消失?这就是我期望 React 一般工作的方式,这意味着 react-bootstrap 中的错误。

或者我应该显式调用 hide() 因为这是他们设计这个组件的方式吗?

以下是重现此内容的一段代码。

谢谢!

var DeleteModal = React.createClass({
    render:function() {
        return (
            <ReactBootstrap.Modal onRequestHide = {this.props.onRequestHide} title = "delete this?">
                <div className="modal-body">
                    Are you sure?
                </div>
                <div className="modal-footer">
                    <button onClick={this.props.onOkClick}>ok</button>
                    <button onClick={this.props.onRequestHide}>cancel</button>
                </div>
            </ReactBootstrap.Modal>
        );
    }
});

var Item = React.createClass({
    _onClick:function() {
        //this.refs.theTrigger.hide();
        this.props.onClick(this.props.name);
    },
    render:function() {
        return (
            <div>
                <span>{this.props.name}</span>
                <ModalTrigger modal={<DeleteModal onOkClick={this._onClick}/>} ref="theTrigger">
                    <button>delete me!</button>
                </ModalTrigger>
            </div>
        );
    }
});

var Content = React.createClass({
    onClick:function(name) {
        this.setState({items:_.reject(this.state.items, function(item) {return item === name;})});
    },
    getInitialState:function() {
        return {items : ["first", "secondth", "thirdst"]};
    },
    render:function() {
        return (
            <div>
                {_.map(this.state.items, function(item, i) {
                    return (
                        <Item name={item} onClick={this.onClick} key={i}/>
                    )}.bind(this)
                )}
            </div>
        );
    }
});

React.render(<Content/>, document.getElementById("mydiv"));

原来是误用了 React 的 "key" 属性。我们给了映射对象整数键,所以当再次调用渲染时,给出了相同的初始键,这就是为什么 React 认为它应该重用相同的 DOM 元素。

如果我们给它 key={item} (其中 item 是一个简单的字符串),它就解决了我们的问题;然而,这引入了一个微妙的错误,如果有 2 个相同的字符串,React 将只显示一个。

试图通过给它 key={item + i} 来打败它会引入一个更微妙的错误,其中显示重复的项目但被大量删除,但在这种情况下,错误出现在 onClick 方法中,它需要进行修改以接受某种索引。

因此我的结论是键必须是唯一的字符串,回调在执行任何修改时都应考虑这些键。