【发布时间】: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-router 和Flux 的帮助下为我的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;
流程
<MainContainer> 组件是一个纯粹的布局组件,它显示一个<TopHeader> 组件和一个<Sidebar> 组件,我希望这两个组件在所有页面中都保持一致。我的<MainContainer>(代码在本说明末尾提供)组件中的附加{this.props.children} 将拉入我的主要内容,其中包括<Home>、<AllVideos> 等组件。
<Home> 组件(可通过 localhost:3000 访问)目前只是占位符 - 我现在专注于 <AllVideos> 组件(可通过 localhost:3000/videos 访问)。这个<MainContainer> 组件触发ComponentWillMount() 中的一个动作,该动作触发一个AJAX 调用,然后我将在我的商店中收到该调用。我填充了我的初始 store 对象并发出了一个 onChange 事件,我在我的 <AllVideos> 组件中监听了该事件
<AllVideos> 组件具有初始 AJAX 调用的侦听器(在上面提到的 <MainContainers> 组件中进行)以及当用户使用表单激活某些过滤器时我发出的 onFilter 事件的侦听器下拉菜单、输入、复选框等元素(在名为<SearchAndFilter> 的单独嵌套组件中)。
<AllVideos> 组件有一个嵌套的<Videos> 组件(它目前除了将 props 传递给它的嵌套 <VideosBody> 组件之外什么都不做)。
在嵌套的<Videos> 组件中,我有一个嵌套的<VideosBody> 组件,它映射通过道具接收的视频对象并创建多个<VideoCard> 组件(以显示每个视频的数据)。
另外在<Videos> 组件中,我有一个<SearchAndFilter> 组件(位于所有渲染的<VieoCard> 组件之上),它将显示一些表单元素,用于对数据/视频进行一些简单的客户端过滤。
<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)链接。它工作正常,我看到我的视频按预期呈现。我尝试使用组件内的表单元素过滤数据。过滤有效,但我得到了我在开头提到的警告/错误,这反过来指向 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);
},
.....
.....
}
【问题讨论】:
标签: reactjs react-router flux