React - setState() 在未安装的组件上
React - setState() on unmounted component
在我的 React 组件中,我试图在 ajax 请求正在进行时实现一个简单的微调器 - 我使用 state 来存储加载状态。
出于某种原因,我的 React 组件中的下面这段代码抛出了这个错误
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.
如果我去掉第一个 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 & 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>
);
}
问题是当组件应该已经安装时为什么我会收到此错误(因为它是从 componentDidMount 调用的)我认为在安装组件后设置状态是安全的?
没有看到渲染功能有点难。虽然已经可以发现您应该做的事情,但每次使用间隔时,您都必须在卸载时清除它。所以:
componentDidMount() {
this.loadInterval = setInterval(this.loadSearches, this.props.pollInterval);
}
componentWillUnmount () {
this.loadInterval && clearInterval(this.loadInterval);
this.loadInterval = false;
}
由于卸载后可能仍会调用成功和错误回调,因此您可以使用间隔变量来检查它是否已挂载。
this.loadInterval && this.setState({
loading: false
});
希望这对您有所帮助,如果这不起作用,请提供渲染功能。
干杯
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 ?
不是从componentDidMount
调用的。您的 componentDidMount
生成一个回调函数,该函数将在计时器处理程序的堆栈中执行,而不是在 componentDidMount
的堆栈中执行。显然,当你的回调 (this.loadSearches
) 被执行时,组件已经卸载。
所以接受的答案会保护你。如果您正在使用其他一些不允许您取消异步函数(已提交给某些处理程序)的异步 API,您可以执行以下操作:
if (this.isMounted())
this.setState(...
这将消除您在所有情况下报告的错误消息,尽管它确实感觉像是在地毯下扫东西,特别是如果您的 API 提供取消功能(如 setInterval
clearInterval
).
为了子孙后代,
在我们的例子中,这个错误与 Reflux、回调、重定向和 setState 有关。我们向 onDone 回调发送了一个 setState,但我们也向 onSuccess 回调发送了一个重定向。在成功的情况下,我们的 onSuccess 回调在 onDone 之前执行。这 在尝试的 setState 之前导致重定向。因此错误,setState 在未安装的组件上。
Reflux 存储操作:
generateWorkflow: function(
workflowTemplate,
trackingNumber,
done,
onSuccess,
onFail)
{...
修复前调用:
Actions.generateWorkflow(
values.workflowTemplate,
values.number,
this.setLoading.bind(this, false),
this.successRedirect
);
修复后调用:
Actions.generateWorkflow(
values.workflowTemplate,
values.number,
null,
this.successRedirect,
this.setLoading.bind(this, false)
);
更多
在某些情况下,由于 React 的 isMounted 是 "deprecated/anti-pattern",我们采用了使用 _mounted 变量并自行监控它。
对于需要其他选项的人,ref 属性的回调方法可以作为解决方法。 handleRef 的参数是对 div DOM 元素的引用。
有关 refs 和 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() {
...
}
}
分享 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)
}, [])
同样的解决方案可以扩展到任何时候你想取消之前关于获取ID更改的请求,否则多个in-flight请求之间会出现竞争条件(this.setState
叫错了)。
React.useEffect(() => {
let isCancelled = false
callApi(id).then(...).catch(...) // similar to above
return () => (isCancelled = true)
}, [id])
感谢 javascript 中的 closures。
总的来说,上面的想法接近于react doc推荐的makeCancelable approach,明确指出
isMounted is an Antipattern
信用
仅供参考。将 CPromise 与装饰器一起使用,您可以执行以下技巧:
(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");
}
}
在我的 React 组件中,我试图在 ajax 请求正在进行时实现一个简单的微调器 - 我使用 state 来存储加载状态。
出于某种原因,我的 React 组件中的下面这段代码抛出了这个错误
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.
如果我去掉第一个 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 & 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>
);
}
问题是当组件应该已经安装时为什么我会收到此错误(因为它是从 componentDidMount 调用的)我认为在安装组件后设置状态是安全的?
没有看到渲染功能有点难。虽然已经可以发现您应该做的事情,但每次使用间隔时,您都必须在卸载时清除它。所以:
componentDidMount() {
this.loadInterval = setInterval(this.loadSearches, this.props.pollInterval);
}
componentWillUnmount () {
this.loadInterval && clearInterval(this.loadInterval);
this.loadInterval = false;
}
由于卸载后可能仍会调用成功和错误回调,因此您可以使用间隔变量来检查它是否已挂载。
this.loadInterval && this.setState({
loading: false
});
希望这对您有所帮助,如果这不起作用,请提供渲染功能。
干杯
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 ?
不是从componentDidMount
调用的。您的 componentDidMount
生成一个回调函数,该函数将在计时器处理程序的堆栈中执行,而不是在 componentDidMount
的堆栈中执行。显然,当你的回调 (this.loadSearches
) 被执行时,组件已经卸载。
所以接受的答案会保护你。如果您正在使用其他一些不允许您取消异步函数(已提交给某些处理程序)的异步 API,您可以执行以下操作:
if (this.isMounted())
this.setState(...
这将消除您在所有情况下报告的错误消息,尽管它确实感觉像是在地毯下扫东西,特别是如果您的 API 提供取消功能(如 setInterval
clearInterval
).
为了子孙后代,
在我们的例子中,这个错误与 Reflux、回调、重定向和 setState 有关。我们向 onDone 回调发送了一个 setState,但我们也向 onSuccess 回调发送了一个重定向。在成功的情况下,我们的 onSuccess 回调在 onDone 之前执行。这 在尝试的 setState 之前导致重定向。因此错误,setState 在未安装的组件上。
Reflux 存储操作:
generateWorkflow: function(
workflowTemplate,
trackingNumber,
done,
onSuccess,
onFail)
{...
修复前调用:
Actions.generateWorkflow(
values.workflowTemplate,
values.number,
this.setLoading.bind(this, false),
this.successRedirect
);
修复后调用:
Actions.generateWorkflow(
values.workflowTemplate,
values.number,
null,
this.successRedirect,
this.setLoading.bind(this, false)
);
更多
在某些情况下,由于 React 的 isMounted 是 "deprecated/anti-pattern",我们采用了使用 _mounted 变量并自行监控它。
对于需要其他选项的人,ref 属性的回调方法可以作为解决方法。 handleRef 的参数是对 div DOM 元素的引用。
有关 refs 和 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() {
...
}
}
分享 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)
}, [])
同样的解决方案可以扩展到任何时候你想取消之前关于获取ID更改的请求,否则多个in-flight请求之间会出现竞争条件(this.setState
叫错了)。
React.useEffect(() => {
let isCancelled = false
callApi(id).then(...).catch(...) // similar to above
return () => (isCancelled = true)
}, [id])
感谢 javascript 中的 closures。
总的来说,上面的想法接近于react doc推荐的makeCancelable approach,明确指出
isMounted is an Antipattern
信用
仅供参考。将 CPromise 与装饰器一起使用,您可以执行以下技巧: (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");
}
}