【问题标题】:ReactJS/Router/Flux: Warning: Can only update a mounted or mounting componentReactJS/Router/Flux:警告:只能更新已安装或正在安装的组件
【发布时间】:2017-06-22 10:25:55
【问题描述】:

注意:这是一个相当长的描述,所以请多多包涵。考虑到可重现的 JSBin 或类似实现所需的组件数量,书面描述似乎是更安全和可行的选择。

TL;DR

在我的Flux(3.1.2)/React(15.4.2)/React-Router(3.0.2) 应用程序中,当我在路线之间导航时,我似乎总是收到以下错误/警告

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.

我的努力

我进行了一些研究,大多数信息似乎都表明这是一个未删除监听事件的情况。我尝试了各种不同的建议,但似乎都没有。

一些背景资料

我的工作是开发一个非常简单的应用程序,主要是为了确保动手学习/实践经验,因为我对 ReactJS 还很陌生。

后端的Node/Express 服务器在React-routerFlux 的帮助下为我的ReactJS 应用程序使用的数据(类似于REST 端点)提供服务。

初始数据存储在SQLite DB 中,只要有请求,我就会以JSON 的形式返回。例如,在加载应用程序时,我向我的服务器发起 AJAX 调用(通过Axios)以返回 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;

流程

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

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

&lt;AllVideos&gt; 组件具有初始 AJAX 调用的侦听器(在上面提到的 &lt;MainContainers&gt; 组件中进行)以及当用户使用表单激活某些过滤器时我发出的 onFilter 事件的侦听器下拉菜单、输入、复选框等元素(在名为&lt;SearchAndFilter&gt; 的单独嵌套组件中)。

&lt;AllVideos&gt; 组件有一个嵌套的&lt;Videos&gt; 组件(它目前除了将 props 传递给它的嵌套 &lt;VideosBody&gt; 组件之外什么都不做)。

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

另外在&lt;Videos&gt; 组件中,我有一个&lt;SearchAndFilter&gt; 组件(位于所有渲染的&lt;VieoCard&gt; 组件之上),它将显示一些表单元素,用于对数据/视频进行一些简单的客户端过滤。

&lt;AllVideos&gt; 嵌套如下:

<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 时,“/”路由被激活,&lt;Home&gt; 容器显示正常。

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

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

这似乎是我在路线之间导航时出现的一个问题。我记录了onFilter() 方法并注意到过滤器事件是由我导航路线的次数触发的。如果我执行Home &gt; 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);
    },
    .....
    .....
}

【问题讨论】:

    标签: reactjs react-router flux


    【解决方案1】:

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

    【讨论】:

    • 谢谢!是的,这确实是问题所在。我花了一天半的时间试图弄清楚这个问题。并认为这只是一个案例:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-04-26
    • 2016-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-02
    • 2019-08-15
    相关资源
    最近更新 更多