React - setState() on unmounted component - javascript

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");
}
}

Related

Can I fetch api every time that props change on ComponentDidUpdate?

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();
}

Getting React to re-render after Promise is completed

I've simplified much of the code as the issue lies in the Promise and async/await part.
I wish for this component to get data from my API and create a plot from it. If the API is still retrieving, I want it to show the loading icon.
class Data extends Component {
state = {};
componentDidMount() {
this.setState({ data: this.getData() });
}
async getData() {
try {
const response = await axios.get('/api/data');
return response.data;
} catch (err) {
return [];
}
}
renderLoading() {
return <Loading/>; // this represents a loading icon
}
renderPlot(data) {
return <Plot data={data}/>; // this represents the plot that needs the data
}
render() {
return {this.state.data
? this.renderLoading()
: this.renderPlot(this.state.data)};
}
}
At the moment, what this does is check this.state.data, sees that it's undefined, and simply display the loading icon forever without ever checking it again. How can I make it recheck once the promise is completed? One issue to note is that renderPlot requires data to be finished, if I call renderPlot while the Promise is still pending, it doesn't process it properly.
Instead of calling setState before the data is ready, call this.getData() in componentDidMount and then call setState once the response data is ready. React will automatically re-render the component with state changes.
class Data extends Component {
state = {}
componentDidMount() {
this.getData()
}
getData() {
axios
.get('/api/data')
.then(({ data }) => {
this.setState({ data })
})
.catch(err => {
this.setState({ error: err.message })
})
}
render() {
return this.state.data
? <Plot data={this.state.data} />
: this.state.error
? <div>{this.state.error}</div>
: <Loading />
}
}
You need to call setState after your async operation completes. Try this in your componentDidMount()
componentDidMount() {
// Wait for the operation to complete before setting data
this.getData().then((d) => {
this.setState({ data: d});
});
}
Your code just sets data to be the Promise of the async operation.

Updating the react state/component correctly?

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();
}}

How to update the state value using setTimeout in React Hooks?

Am trying to reset the state value after some time using setTimeout function.
const SetTimeout = props => {
let [errorText, setError] = useState(
props.errorMessage ? props.errorMessage : ""
);
useEffect(() => {
if (errorText !== "") {
setTimeout(() => {
setError("");
}, 4000);
} else {
// console.log("no error")
}
}, [errorText]);
return <div>{errorText}</div>;
};
const MapStateToProps = state => {
return {
errorMessage: state.errorReducer.error
};
};
Am getting the errorMessage from an api call, after some time i'm trying to make that message to empty using useState.
But the component is getting re-rendered with the same error message. could you please help me in fixing this?
You are mixing component state and redux state here which is pretty bad and why you are seeing this behaviour.
You are initially rendering the component with the error message from within Redux, and passing this into the component as a property value. The component displays this, set's it's own state value. After the timeout runs, it's updating it's own state, which causes a re-render, and the Redux value is again passed into the component via the properties.
I would suggest you forget about using useState and use a Redux action to clear the error message that it is storing.
you are using redux to not use state in react components and here you are using state in your component. so what you can do is to add the erromessage to your global state :
state ={
...,
errormessage:undefined
}
and then in your action you need to change your state.errormessage like this:
export const show_error = message => {
return {
type: "SHOW_ERROR",
payload: message
};
};
export const remove_error = () => {
return {
type:"REMOVE_ERROR"
};
};
imagine calling post api to add post using redux thunk you can do it like this:
export function add_post(data) {
return function(dispatch) {
return axios
.post('/posts', data)
.then(res => {
dispatch(fetch_posts_data());})
.catch(err => dispatch(
show_error(err)
);
setTimeout(function() {
dispatch(remove_error());
}, 2000););
};
}
in your reducer :
case "SHOW_ERROR":
return {
...state,
posts: { ...state.posts, errormessage: action.message}
};
case "REMOVE_ERROR":
return {
...state,
posts: { ...state.posts, errormessage: undefined }
};
now to retrieve the errormessage in your component you can use normal connect method but here i will use redux hooks useSelector():
const Error = ()=>{
const errormessage = useSelector(state=>state.errormessage)
return(
<>
{
errormessage?
<div>{errormessage}</div>
:""
}
</>
)
}
I've just found this out! So it may not be perfect.
Hooks have a useSelector and a useDispatch (from react-redux)
To stick with having your error in Redux, you component would look like this:
const SetTimeout = () => {
const errorText = useSelector(state => state.errorReducer.error);
const clearError = useDispatch();
useEffect(() => {
if (errorText !== "") {
const timeout = setTimeout(() => {
clearError({ type: "CLEAR_ERROR" });
}, 5000);
} else {
// console.log("no error")
}
// this will clear Timeout when component unmounts like in willComponentUnmount
return () => { clearTimeout(timeout); };
}, []);
return <div style={{ color: "red" }}>{errorText}</div>;
};
export default SetTimeout;
Pretty cool functional component without the redux boiler plate code.
Hope that helps

How to call a function that uses the state (which is set by another function) and updates state

I have two functions one that fetches data from an api and updates state according to that data, and a function that itterates over the data in the state and updates the state with the new data.
My problem is that i cant update the state in the second function. And i dont know where i have to call this function in order for it to be called after the first function and to use the data thats in the state.
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
});
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
addPhotos() {
const newCases = this.state.cases.map(({ photo_id, ...rest }) => {
const obj = { ...rest };
this.state.photos.find(data => {
if (data.id === photo_id) {
obj.file = data.file;
return true;
}
});
return obj;
});
console.log(this.state.cases);
this.setState({
'cases' : newCases
});
}
renderCases() {
this.addPhotos();
}
componentWillMount() {
this.getCases();
}
render() {
return (
<div>
{this.renderCases()}
</div>
)
}
}
This is what i now have
Where should i call the addPhotos function so it can update the state and still use the existing state data from the getCases function?
Thanks in advance!
So, first thing's first. The lifecycle method componentWillMount() is soon to be deprecated and is considered unsafe to use. You should be using componentDidMount().
As far as using the updated state in your addPhotos function, you can pass setState a callback function. A seemingly simple solution would be to just pass the addPhotos function as a callback into the setState being called in your getCases function.
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
}, this.addPhotos);
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
Another solution would be to call addPhotos() from componentDidUpdate instead.
Hope this helps!
Edit: Just some additional background information from the React docs.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
Added some refactoring to your code, should work ok now, read comments for details
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(this.addPhotos) // don't need to set state, pass data to consumer function
.catch(console.error); // catch always gives error, don't need to check with if statement
}
addPhotos(response) {
const cases = response.data.cases // extract values
const photos = response.data.photos // extract values
// your .map iterator has O^2 complexity (run each item of cases on each item of photos)
// this new .map iterator has O complexity (run each item of cases)
const newCases = cases.map(({ photo_id, ...rest }) => {
const obj = {...rest};
const data = photos.find(item => item.id === photo_id);
if (data) {
obj.file = data.file
}
return obj;
});
this.setState({
cases: newCases,
photos
});
}
componentDidMount() { // better use didMount
this.getCases();
}
render() {
return (<div />)
}
}

Categories