Note: This is a rather long description, so do bear with me. With the number of components that is entailed for a reproducible JSBin or similar implementation, a written description seemed to be a safer and workable choice.
TL;DR
In my Flux(3.1.2)
/ React(15.4.2)
/ React-Router(3.0.2)
app, i always seem to get the following error/warning ONLY when i navigate between routes:
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.
My effort
I researched a bit and most of the information seems to point to it being a case of listened events not being removed. I tried all variations of suggestions but none seem to work.
Some background information
My effort is a very simple app being developed mainly to ensure hands-on learning/practice experience since i am quite new to ReactJS.
A Node
/ Express
server in the backend serves data (something like a REST endpoint) consumed by my ReactJS
app aided by React-router
and Flux
.
The initial data is stored in an SQLite
DB that i return back as JSON
whenever requested. For example on loading the app, i initiate an AJAX call(via Axios
) to my server to return back a subset of results in JSON which is then stored in my Flux
store. Stateful components will use this data to manipulate the view as required.
My Routes.js has this structure:
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;
The flow
The <MainContainer>
component is a purely layout component that displays a <TopHeader>
component and a <Sidebar>
component, both of which i want consistently persistent across all pages. The additional {this.props.children}
in my <MainContainer>
(code is provided towards the end of this description) component will pull in my main content which includes components like <Home>
, <AllVideos>
etc.
The <Home>
component(reachable via localhost:3000
) is currently just placeholder - i am concentrating on the <AllVideos>
component (reachable via localhost:3000/videos
) for now. This <MainContainer>
component triggers an action in ComponentWillMount()
that fires off an AJAX call that i will then receive in my store. I populate my initial store object and emit a onChange
event that I listen for in my <AllVideos>
component
The <AllVideos>
component has listeners for the initial AJAX call (made in the <MainContainers>
component as mentioned above) as well as listeners for a onFilter
event that i emit when certain filters are activated by the user using form elements like dropdowns, inputs, checkboxes etc (in a separate nested component called <SearchAndFilter>
).
The <AllVideos>
component has a nested <Videos>
component(which currently does nothing but passing on props to its nested <VideosBody>
component).
Inside the nested <Videos>
component i have a nested <VideosBody>
component which maps through the videos object received via props and creates several <VideoCard>
components(to display data per video).
Additionally in the <Videos>
component, i have a single <SearchAndFilter>
component (sitting above all the rendered <VieoCard>
components)that will display a few form elements for some simple client-side filtering of the data/videos.
The <AllVideos>
nesting is thus:
<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>
When i hit http://localhost:3000
, the "/" route is activated and the <Home>
container display fine.
I navigate(<Link to>
) from the Sidebar to the videos(http://localhost:3000/videos) link which displays the component(and thus all nested components inside this). It works fine, i see my videos rendered as expected. I attempt to filter the data using the form elements inside the component. The filteration works but i get the warning/error that i mentioned in the beginning, which in turn points to the onFilter() method as the culprit (i.e the method that triggers when the Filter event is emitted).
When i directly hit http://localhost:3000/videos
, my component displays with correct data. My filters work fine and no errors or warnings are emitted. It works just as i want it to.
It seems to be an issue that emerges when i navigate between routes. I logged the onFilter()
method and noticed that the filter event is triggered by the number of times i have navigated the routes. If i do a Home > Videos
it gets correctly triggered once. I navigate back to Home and come back to videos and re-initiate the filter and now the onFilter()
method is now called twice (with twice the errors). This keeps increasing with each navigation performed.
I noticed that the ComponentWillUnMount()
doesn't seem to fire in any of the effected components when i navigate between videos and anywhere else. This method is where i remove the event listeners. However, the ComponentWillMount()
and ComponentDidMount()
seems to fire with each navigation initiated in all the components. A similar issue is described here (sub-routes)
My MainContainer component(for layout):
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>
);
};
My App component:
export default class App extends Component {
constructor(props){
super(props);
this.state = {
}
};
...
...
render() {
return (
<div>
{this.props.children}
</div>
);
};
};
The AllVideos component:
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>
);
};
The Dispatched events received:
_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;
};
});
The event emiiters:
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);
},
.....
.....
}
I'm on phone so I can't do any testing of your code, but what if you lowercased the 'M' in componentWillUnmount
?