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' 小写怎么办?
注意:这是一个相当长的描述,所以请耐心等待。对于可重现的 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' 小写怎么办?