未安装组件上的 React-setState()

在我的反应组件中,我试图在 Ajax 请求进行中实现一个简单的 spinner ——我使用状态来存储加载状态。

由于某种原因,我的 React 组件中的下面这段代码抛出了这个错误

只能更新已挂载或正在挂载的组件。这通常意味着 您在未挂载的组件上调用 setState ()。 请检查未定义组件的代码。

如果我去掉第一个 setState 调用,错误就消失了。

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 &amp; 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>
);
}

问题是,为什么当组件应该已经被挂载(因为它是从 Component entDidMount 调用的)时,我得到了这个错误,我认为一旦组件被挂载,设置状态是安全的?

103200 次浏览

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).

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.

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/

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