ReactJS/Router/Flux:警告:只能更新已安装或安装的组件

ReactJS/Router/Flux: Warning: Can only update a mounted or mounting component

注意:这是一个相当长的描述,所以请耐心等待。对于可重现的 JSBin 或类似实现所需的组件数量,书面描述似乎是更安全和可行的选择。

TL;DR

在我的 Flux(3.1.2) / React(15.4.2)/ React-Router(3.0.2) 应用程序中,我似乎总是得到以下 error/warning ONLY 时我在路线之间导航:

warning.js:36 Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the AllVideos component.

我的努力

我进行了一些研究,大部分信息似乎都表明这是一个未删除已收听事件的情况。我尝试了各种建议,但 none 似乎有效。

一些背景资料

我的努力是开发一个非常简单的应用程序,主要是为了确保动手 learning/practice 体验,因为我对 ReactJS 还很陌生。

后端的 Node/Express 服务器在 React-router 和 [=26] 的帮助下提供我的 ReactJS 应用程序使用的数据(类似于 REST 端点) =].

初始数据存储在 SQLite 数据库中,每当请求时我 return 返回为 JSON。例如,在加载应用程序时,我向我的服务器发起一个 AJAX 调用(通过 Axios),以 return 返回 JSON 中的结果子集,然后将其存储在我的Flux 商店。有状态组件将使用此数据根据需要操作视图。

我的 Routes.js 有这样的结构:

const Routes = (props) => ( 
  <Router {...props}>
   <Route component={MainContainer}> 
    <Route path="/" component={App}> 
     <IndexRoute component={Home} />
     <Route path="videos" component={Videos}></Route>
     <Route path="videos/:mediaid" component={MediaDetails} />
   </Route>
   <Route path="*" component={NotFound} />
  </Route>
</Router>
);  
export default Routes;

流量

<MainContainer> 组件是一个纯粹的布局组件,它显示一个 <TopHeader> 组件和一个 <Sidebar> 组件,我希望这两个组件在所有页面上始终保持一致。 <MainContainer>(代码在本说明末尾提供)组件中的附加 {this.props.children} 将提取我的主要内容,其中包括 <Home><AllVideos> 等组件。

<Home> 组件(可通过 localhost:3000 访问)目前只是占位符 - 我现在专注于 <AllVideos> 组件(可通过 localhost:3000/videos 访问)。这个 <MainContainer> 组件触发了 ComponentWillMount() 中的一个动作,该动作触发了一个 AJAX 调用,然后我将在我的商店中收到该调用。我填充我的初始存储对象并发出一个 onChange 事件,我在 <AllVideos> 组件

中监听该事件

<AllVideos> 组件具有用于初始 AJAX 调用的侦听器(如上所述在 <MainContainers> 组件中进行)以及用于 onFilter 事件的侦听器当用户使用下拉列表、输入、复选框等表单元素(在一个名为 <SearchAndFilter> 的单独嵌套组件中)激活某些过滤器时,我会发出。

<AllVideos> 组件有一个嵌套的 <Videos> 组件(目前除了将 props 传递给其嵌套的 <VideosBody> 组件外什么都不做)。

在嵌套的 <Videos> 组件中,我有一个嵌套的 <VideosBody> 组件,它映射通过道具接收的视频对象,并创建几个 <VideoCard> 组件(以显示每个视频的数据)。

此外,在 <Videos> 组件中,我有一个 <SearchAndFilter> 组件(位于所有呈现的 <VieoCard> 组件之上),它将为一些简单的客户端显示一些表单元素- data/videos 的侧过滤。

<AllVideos> 嵌套是这样的:

<AllVideos> (listens for the initial AJAX event triggered from a parent component as well as Filter events triggered from a child component, passes props of its state to its nested children)
  <Videos> (currently just relays props received - may do other things later)
    <VideosBody> (uses props to build the videocards via looping through the data)
        <SearchAndFilter /> (onchange of form element values, fire-and-forget actions are triggered that are dispatched to the store where a Filter event is emitted once the processing is done (i use `promises` since there are keydown events)
        <VideoCard /><VideoCard /><VideoCard /><VideoCard /> etc (also has a <Link to> to the <MediaDetails> object
    </VideosBody>
  </Videos>
</AllVideos>

当我点击 http://localhost:3000 时,“/”路径被激活并且 <Home> 容器显示正常。

我从侧边栏导航(<Link to>) 到视频(http://localhost:3000/videos) link,它显示组件(以及其中的所有嵌套组件)。它工作正常,我看到我的视频按预期呈现。我尝试使用组件内的表单元素来过滤数据。过滤有效,但我得到了我在开头提到的 warning/error,它又指向 onFilter() 方法作为罪魁祸首(即在发出 Filter 事件时触发的方法)。

当我直接点击 http://localhost:3000/videos 时,我的组件显示正确的数据。我的过滤器工作正常,没有发出任何错误或警告。它就像我想要的那样工作。

这似乎是我在路线之间导航时出现的问题。我记录了 onFilter() 方法并注意到过滤器事件是由我导航路线的次数触发的。如果我执行 Home > Videos 它会正确触发一次。我导航回主页并返回视频并重新启动过滤器,现在 onFilter() 方法现在被调用了两次(错误是两倍)。这随着每次导航的执行而不断增加。

我注意到当我在视频和其他任何地方之间导航时,ComponentWillUnMount() 似乎没有在任何受影响的组件中触发。这个方法是我删除事件监听器的地方。但是,ComponentWillMount()ComponentDidMount() 似乎随着所有组件中启动的每个导航而触发。一个类似的问题is described here (sub-routes)

我的 MainContainer 组件(用于布局):

export default class MainContainer extends React.Component{
...
    componentDidMount(){
        CatalogActions.GetCatalog(); //gets my initial AJAX call going
    };
...
render(){
        return(
        <div className="container-fluid">
            <header className="row primary-header">
                <TopHeader />
            </header>
            <aside className="row primary-aside">Aside here </aside>
            <main className="row">
                <Sidebar />
                <div className="col-md-9 no-float">
                    {this.props.children}
                </div>
            </main>

        </div>
        );
    };

我的应用组件:

export default class App extends Component {
constructor(props){

    super(props);

    this.state = {

  }
};

...
...

render() {
  return (
    <div>
        {this.props.children}
    </div>
    );
    };
};

AllVideos 组件:

export default class AllVideos extends React.Component {
    constructor(props){
        super(props);
        this.state={
           // isVisible: true
             initialDataUpdated:false,
             isFiltered: false,
            "catalog": []
        };
        this.onChange= this.onChange.bind(this);
        this.onError=this.onError.bind(this);
        this.onFilter=this.onFilter.bind(this);
        this.onFilterError=this.onFilterError.bind(this);
    };
    componentDidMount(){
        if(CatalogStore.getList().length>0){
            this.setState({
            "catalog": CatalogStore.getList()[0].videos
        })
        }        
        CatalogStore.addChangeListener(this.onChange);
        CatalogStore.addErrorListener(this.onError);
        CatalogStore.addFilterListener(this.onFilter);
        CatalogStore.addFilterErrorListener(this.onFilterError);
    };
    componentWillUnMount(){ //this method never gets called
        CatalogStore.removeChangeListener(this.onChange);
        CatalogStore.removeErrorListener(this.onError);
        CatalogStore.removeFilterListener(this.onFilter);
        CatalogStore.removeFilterListener(this.onFilterError);
    };
    onChange(){
        //This gets triggered after my initial AJAX call(triggered in the MainContainer component) succeeds
    //Works fine and state is set
        this.setState({
            initialDataUpdated: true,
            "catalog": CatalogStore.getList()[0].videos
        })
    };
    onError(str){
        this.setState({
            initialDataUpdated: false,
            "catalog": CatalogStore.getList()[0].videos

        });
    };
    componentWillMount(){        
    };
    onFilter(){//This is the method that the error points to. The setState does seem to work however, but with accumulating warnings and calls (eg 3 calls if i navigate between different components 3 times instead of the correct 1 that happens when i come to the URL directly without any route navigation)
        this.setState({
            //isFiltered: true,
            catalog: CatalogStore.getFilteredData()
        })

    }
    onFilterError(){
        this.setState({
            //isFiltered: false,
            catalog: CatalogStore.getFilteredData()
        })
    }

/*componentWillReceiveProps(nextProps){
        //console.log(this.props);
    };
    shouldComponentUpdate(){
        //return this.state.catalog.length>0
        return true
    }
    componentWillUpdate(){
        //console.log("Will update")
    }    
*/
    render(){

            return(
            <div style={VideosTabStyle}>
                This is the All Videos tab.
                <Videos data={this.state.catalog} />

            </div>
        ); 
    };

收到的 Dispatched 事件:

_videoStore.dispatchToken = CatalogDispatcher.register(function(payload) {
    var action = payload.action;
    switch (action.actionType) {
        case CatalogConstants.GET_INITIAL_DATA:
            _videoStore.list.push(action.data);
            _filteredStore.videos.push(action.data.videos);
            _filteredStore.music.push(action.data.music);
            _filteredStore.pictures.push(action.data.pictures);
            catalogStore.emit(CHANGE_EVENT);
            break;
        case CatalogConstants.GET_INITIAL_DATA_ERROR:
            catalogStore.emit(ERROR_EVENT);
            break;
        case CatalogConstants.SEARCH_AND_FILTER_DATA:
            catalogStore.searchFilterData(action.data).then(function() {
              //emits the event after processing - works fine
                catalogStore.emit(FILTER_EVENT);
            },function(err){
                catalogStore.emit(FILTER_ERROR_EVENT);
            });
            break;
        .....
        .....    
        default:
            return true;
    };
});

事件发射器:

var catalogStore = Object.assign({}, EventEmitter.prototype, {
    addChangeListener: function(cb) {
        this.on(CHANGE_EVENT, cb);
    },
    removeChangeListener: function(cb) {
        this.removeListener(CHANGE_EVENT, cb);
    },
    addErrorListener: function(cb) {
        this.on(ERROR_EVENT, cb);
    },
    removeErrorListener: function(cb) {
        this.removeListener(ERROR_EVENT, cb);
    },

    addFilterListener: function(cb) {
        this.on(FILTER_EVENT, cb);
    },
    removeFilterListener: function(cb) {
        this.removeListener(FILTER_EVENT, cb);
    },
    addFilterErrorListener: function(cb) {
        this.on(FILTER_ERROR_EVENT, cb);
    },
    removeFilterErrorListener: function(cb) {
        this.removeListener(FILTER_ERROR_EVENT, cb);
    },
    .....
    .....
}

我在 phone,所以我无法对您的代码进行任何测试,但是如果您将 componentWillUnmount 中的 'M' 小写怎么办?