As you see my code, I want to fetch API contentUrl every its change from props.
but browser throw error like this. Have someone help me?.
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in the
componentWillUnmount method.
Issue is clearly states that : Can't perform a React state update on
an unmounted component
So check if the component is unmounted before setting up state, _isMounted
Code Snippet Ref from : HERE , Hope this will clear all your doubts
class News extends Component {
_isMounted = false; // <----- HERE
constructor(props) {
super(props);
this.state = {
news: [],
};
}
componentDidMount() {
this._isMounted = true; // <----- HERE
axios
.get('YOUR_URL')
.then(result => {
if (this._isMounted) { // <----- CHECK HERE BEFORE SETTING UP STATE
this.setState({
news: result.data.hits,
});
}
});
}
componentWillUnmount() {
this._isMounted = false; // <----- HERE
}
render() {
...
}
}
For example, if a call is done and your component is unmounted, a setState will be called.
You can prevent this with a condition:
_isMount = true;
componentDidUpdate() {
this.props.getContentJSON(url).then(() => {
if(this._isMount){
this.setState({...})
}
})
}
componentWillUnmount() {
this._isMount = false;
}
or control your call :
controller = new AbortController();
componentDidUpdate() {
// I use fetch here but you can adapte to your code
fetch(url, { signal: controller.signal }).then(() => {
this.setState({...})
})
}
componentWillUnmount() {
controller.abort();
}
Related
I am trying to make my component reactive on updates. I am using componentDidUpdate() to check if the components prop state has changed, then if it has it is has I need the getPosts() function to be called and the postCount to update if that prop is changed.
export default class JsonFeed extends React.Component<IJsonFeedProps, IJsonFeedState> {
// Props & state needed for the component
constructor(props) {
super(props);
this.state = {
description: this.props.description,
posts: [],
isLoading: true,
jsonUrl: this.props.jsonUrl,
postCount: this.props.postCount,
errors: null,
error: null
};
}
// This function runs when a prop choice has been updated
componentDidUpdate() {
// Typical usage (don't forget to compare props):
if (this.state !== this.state) {
this.getPosts();
// something else ????
}
}
// This function runs when component is first renderd
public componentDidMount() {
this.getPosts();
}
// Grabs the posts from the json url
public getPosts() {
axios
.get("https://cors-anywhere.herokuapp.com/" + this.props.jsonUrl)
.then(response =>
response.data.map(post => ({
id: `${post.Id}`,
name: `${post.Name}`,
summary: `${post.Summary}`,
url: `${post.AbsoluteUrl}`
}))
)
.then(posts => {
this.setState({
posts,
isLoading: false
});
})
// We can still use the `.catch()` method since axios is promise-based
.catch(error => this.setState({ error, isLoading: false }));
}
You can change componentDidUpdate to:
componentDidUpdate() {
if (this.state.loading) {
this.getPosts();
}
}
This won't be an infinite loop as the getPosts() function sets state loading to false;
Now every time you need an update you just need to set your state loading to true.
If what you want to do is load everytime the jsonUrl updates then you need something like:
componentDidUpdate(prevProps) {
if (prevProps.jsonUrl!== this.props.jsonUrl) {
this.getPosts();
}
}
Also I don't get why you expose your components state by making componentDidMount public.
Modify your getPosts to receive the jsonUrl argument and add the following function to your class:
static getDerivedStateFromProps(props, state) {
if(props.jsonUrl!==state.jsonUrl){
//pass jsonUrl to getPosts
this.getPosts(props.jsonUrl);
return {
...state,
jsonUrl:props.jsonUrl
}
}
return null;
}
You can get rid of the componentDidUpdate function.
You can also remove the getPosts from didmount if you don't set state jsonUrl in the constructor.
// This function runs when a prop choice has been updated
componentDidUpdate(prevProps,prevState) {
// Typical usage (don't forget to compare props):
if (prevState.jsonUrl !== this.state.jsonUrl) {
this.getPosts();
// something else ????
}
}
this way you have to match with the updated state
Try doing this
componentDidUpdate(prevState){
if(prevState.loading!==this.state.loading){
//do Something
this.getPosts();
}}
Gutentag, guys!
I keep getting this error message from my application after unmounting the component:
Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
in Header (at index.js:27)
Now here is the code from the Header component:
class Header extends Component {
isCancelled = false;
state = {
someStateVars: x,
separateColumns: 'true',
}
handleChange = (event) => {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
if (!this.isCancelled) {
this.setState({ //######THIS IS LINE 27######
[name]: value
});
}
}
handleDisplayChange = (event) => {
const value = event.target.value;
const name = 'separateColumns';
if (!this.isCancelled) {
this.setState({
[name]: value
}, () => {
this.props.displayChange();
});
}
}
serversRefresh = () => {
if (!this.isCancelled) {
setTimeout(() => {
this.setState({refreshed: false});
}, localStorage.getItem('seconds')*1000); //disable refresh for 15 seconds
}
}
reactivateButton = () => {
if (!this.isCancelled) this.setState({refreshed: false});
}
componentDidMount() {
if(localStorage.getItem('seconds')>5 && !this.isCancelled){
this.setState({refreshed: true});
}
}
componentWillUnmount() {
this.isCancelled = true;
}
}
When I saw I was getting this error, I've added isCancelled variable which is changed in componentWillUnmount() function to true.
After I unmount the Header component, after 15 seconds, when the serversRefreshbutton is reactivated, I get this error message.
How can I fix it?
On another component where I've encountered this issue "isCancelled" var did help, but here I see it has no impact and the issue persists.
Just store your timeout in a variable, e.g.
this.timeout = setTimeout(/* your actions here*/, /* your timeout */)
and then clear your timeout in the componentWillUnmount
componentWillUnmount() {
clearTimeout(this.timeout)
}
It should fix your problem without crutches like this.isCancelled. Detecting component's mount state is no-op, because it still unloaded from memory even after unmounting.
setTimeout returns timer's id with which it can be cancelled in future. clearTimeout cancels timeout by it's id, if it is not yet executed.
More about your case you can read here: Why isMounted is antipattern.
More about timers on MDN.
In a lot of my components I need to do something like this:
handleSubmit() {
this.setState({loading: true})
someAsyncFunc()
.then(() => {
return this.props.onSuccess()
})
.finally(() => this.setState({loading: false}))
}
The onSuccess function
may or may not be a promise (if it is, loading should stay true until it is resolved)
may or may not unmount the component (it may close the modal this component is in or even navigate to different page)
If the function unmounts the component, this.setState({loading: false}) obviously triggers a warning Can't call setState (or forceUpdate) on an unmounted component.
My 2 questions:
Is there a simple way to avoid the issue ? I don't want to set some _isMounted variable in componentDidMount and componentWillUnmount and then check it when needed in most of my components, plus I may forget to do it next time writing something like this ...
Is it really a problem ? I know that, according to the warning, it indicates a memory leak in my application, but it is not a memory leak in this case, is it ? Maybe ignoring the warning would be ok ...
EDIT: The second question is a little bit more important for me than the first. If this really is a problem and I just can't call setState on unmounted component, I'd probably find some workaround myself. But I am curious if I can't just ignore it.
Live example of the problem:
const someAsyncFunc = () => new Promise(resolve => {
setTimeout(() => {
console.log("someAsyncFunc resolving");
resolve("done");
}, 2000);
});
class Example extends React.Component {
constructor(...args) {
super(...args);
this.state = {loading: false};
}
componentDidMount() {
setTimeout(() => this.handleSubmit(), 100);
}
handleSubmit() {
this.setState({loading: true})
someAsyncFunc()
/*
.then(() => {
return this.props.onSuccess()
})
*/
.finally(() => this.setState({loading: false}))
}
render() {
return <div>{String(this.state.loading)}</div>;
}
}
class Wrapper extends React.Component {
constructor(props, ...rest) {
super(props, ...rest);
this.state = {
children: props.children
};
}
componentDidMount() {
setTimeout(() => {
console.log("removing");
this.setState({children: []});
}, 1500)
}
render() {
return <div>{this.state.children}</div>;
}
}
ReactDOM.render(
<Wrapper>
<Example />
</Wrapper>,
document.getElementById("root")
);
.as-console-wrapper {
max-height: 100% !important;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
Unfortunately you have to keep track of "isMounted" yourself.
To simplify you control flow you could use async/await:
handleSubmit() {
this.setState({loading: true})
try {
await someAsyncFunction()
await this.props.onSuccess()
} finally {
if (this._isMounted) {
this.setState({loading: false})
}
}
}
This is actually mentioned in the react docs, which points to this solution: https://gist.github.com/bvaughn/982ab689a41097237f6e9860db7ca8d6
If your someAsyncFunction supports cancelation, you should do so in componentWillUnmount, as encouraged by this article. But then - of course - check the return value and eventually not call this.props.onSuccess.
class myClass extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentDidMount() {
this._isMounted = true;
this._getData();
}
componentWillUnmount() {
this._isMounted = false;
}
_getData() {
axios.get('example.com').then(data => {
if (this._isMounted) {
this.setState({ data })
}
});
}
render() {
...
}
}
You should be able to use this._isMounted to check if the component is actually mounted.
handleSubmit() {
this.setState({loading: true})
someAsyncFunc()
.then(() => {
return this.props.onSuccess()
})
.finally(() => {
if (this && this._isMounted) { // check if component is still mounted
this.setState({loading: false})
}
})
}
But be aware that this approach is considered to be an anitpattern. https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
What about
componentWillUnmount() {
// Assign this.setState to empty function to avoid console warning
// when this.setState is called on an unmounted component
this.setState = () => undefined;
}
I have the following autorun function in my componentDidMount:
componentDidMount() {
this.autoUpdate = autorun(() => {
this.setState({
rows: generateRows(this.props.data)
})
})
}
The problem is that another component changes this.props.data when the component is not mount - and therefor I get the .setState warning on an unmounted component.
So I would like to remove the autorun once the component unmounts.
I tried doing:
componentWillUnmount() {
this.autoUpdate = null
}
But the autorun function still triggers. Is there a way to cancel the mobx autorun once the component is not mounted any more?
autorun returns a disposer function that you need to call in order to cancel it.
class ExampleComponent extends Component {
componentDidMount() {
this.autoUpdateDisposer = autorun(() => {
this.setState({
rows: generateRows(this.props.data)
});
});
}
componentWillUnmount() {
this.autoUpdateDisposer();
}
render() {
// ...
}
}
In my react component im trying to implement a simple spinner while an ajax request is in progress - im using state to store the loading status.
For some reason this piece of code below in my React component throws this error
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 undefined component.
If I get rid of the first setState call the error goes away.
constructor(props) {
super(props);
this.loadSearches = this.loadSearches.bind(this);
this.state = {
loading: false
}
}
loadSearches() {
this.setState({
loading: true,
searches: []
});
console.log('Loading Searches..');
$.ajax({
url: this.props.source + '?projectId=' + this.props.projectId,
dataType: 'json',
crossDomain: true,
success: function(data) {
this.setState({
loading: false
});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
this.setState({
loading: false
});
}.bind(this)
});
}
componentDidMount() {
setInterval(this.loadSearches, this.props.pollInterval);
}
render() {
let searches = this.state.searches || [];
return (<div>
<Table striped bordered condensed hover>
<thead>
<tr>
<th>Name</th>
<th>Submit Date</th>
<th>Dataset & Datatype</th>
<th>Results</th>
<th>Last Downloaded</th>
</tr>
</thead>
{
searches.map(function(search) {
let createdDate = moment(search.createdDate, 'X').format("YYYY-MM-DD");
let downloadedDate = moment(search.downloadedDate, 'X').format("YYYY-MM-DD");
let records = 0;
let status = search.status ? search.status.toLowerCase() : ''
return (
<tbody key={search.id}>
<tr>
<td>{search.name}</td>
<td>{createdDate}</td>
<td>{search.dataset}</td>
<td>{records}</td>
<td>{downloadedDate}</td>
</tr>
</tbody>
);
}
</Table >
</div>
);
}
The question is why am I getting this error when the component should already be mounted (as its being called from componentDidMount) I thought it was safe to set state once the component is mounted ?
Without seeing the render function is a bit tough. Although can already spot something you should do, every time you use an interval you got to clear it on unmount. So:
componentDidMount() {
this.loadInterval = setInterval(this.loadSearches, this.props.pollInterval);
}
componentWillUnmount () {
this.loadInterval && clearInterval(this.loadInterval);
this.loadInterval = false;
}
Since those success and error callbacks might still get called after unmount, you can use the interval variable to check if it's mounted.
this.loadInterval && this.setState({
loading: false
});
Hope this helps, provide the render function if this doesn't do the job.
Cheers
The question is why am I getting this error when the component should already be mounted (as its being called from componentDidMount) I thought it was safe to set state once the component is mounted ?
It is not called from componentDidMount. Your componentDidMount spawns a callback function that will be executed in the stack of the timer handler, not in the stack of componentDidMount. Apparently, by the time your callback (this.loadSearches) gets executed the component has unmounted.
So the accepted answer will protect you. If you are using some other asynchronous API that doesn't allow you to cancel asynchronous functions (already submitted to some handler) you could do the following:
if (this.isMounted())
this.setState(...
This will get rid of the error message you report in all cases though it does feel like sweeping stuff under the rug, particularly if your API provides a cancel capability (as setInterval does with clearInterval).
To whom needs another option, the ref attribute's callback method can be a workaround. The parameter of handleRef is the reference to div DOM element.
For detailed information about refs and DOM: https://facebook.github.io/react/docs/refs-and-the-dom.html
handleRef = (divElement) => {
if(divElement){
//set state here
}
}
render(){
return (
<div ref={this.handleRef}>
</div>
)
}
class myClass extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentDidMount() {
this._isMounted = true;
this._getData();
}
componentWillUnmount() {
this._isMounted = false;
}
_getData() {
axios.get('https://example.com')
.then(data => {
if (this._isMounted) {
this.setState({ data })
}
});
}
render() {
...
}
}
Share a solution enabled by react hooks.
React.useEffect(() => {
let isSubscribed = true
callApi(...)
.catch(err => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed, ...err }))
.then(res => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed }))
.catch(({ isSubscribed, ...err }) => console.error('request cancelled:', !isSubscribed))
return () => (isSubscribed = false)
}, [])
the same solution can be extended to whenever you want to cancel previous requests on fetch id changes, otherwise there would be race conditions among multiple in-flight requests (this.setState called out of order).
React.useEffect(() => {
let isCancelled = false
callApi(id).then(...).catch(...) // similar to above
return () => (isCancelled = true)
}, [id])
this works thanks to closures in javascript.
In general, the idea above was close to the makeCancelable approach recommended by the react doc, which clearly states
isMounted is an Antipattern
Credit
https://juliangaramendy.dev/use-promise-subscription/
For posterity,
This error, in our case, was related to Reflux, callbacks, redirects and setState. We sent a setState to an onDone callback, but we also sent a redirect to the onSuccess callback. In the case of success, our onSuccess callback executes before the onDone. This causes a redirect before the attempted setState. Thus the error, setState on an unmounted component.
Reflux store action:
generateWorkflow: function(
workflowTemplate,
trackingNumber,
done,
onSuccess,
onFail)
{...
Call before fix:
Actions.generateWorkflow(
values.workflowTemplate,
values.number,
this.setLoading.bind(this, false),
this.successRedirect
);
Call after fix:
Actions.generateWorkflow(
values.workflowTemplate,
values.number,
null,
this.successRedirect,
this.setLoading.bind(this, false)
);
More
In some cases, since React's isMounted is "deprecated/anti-pattern", we've adopted the use of a _mounted variable and monitoring it ourselves.
Just for reference. Using CPromise with decorators you can do the following tricks:
(Live demo here)
export class TestComponent extends React.Component {
state = {};
#canceled(function (err) {
console.warn(`Canceled: ${err}`);
if (err.code !== E_REASON_DISPOSED) {
this.setState({ text: err + "" });
}
})
#listen
#async
*componentDidMount() {
console.log("mounted");
const json = yield this.fetchJSON(
"https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"
);
this.setState({ text: JSON.stringify(json) });
}
#timeout(5000)
#async
*fetchJSON(url) {
const response = yield cpFetch(url); // cancellable request
return yield response.json();
}
render() {
return (
<div>
AsyncComponent: <span>{this.state.text || "fetching..."}</span>
</div>
);
}
#cancel(E_REASON_DISPOSED)
componentWillUnmount() {
console.log("unmounted");
}
}