您的位置:首页 > Web前端 > React

react系列之isMounted is an Antipattern

2016-11-10 15:31 302 查看
用了一年多的React,真是爽的不要不要的, 谁用谁知道, 一般人我不告诉他!

最近用的过程中发现console里面总是出现这样的警告

react.js:20478 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 Small component.

虽不影响使用, 但是对于一个有代码洁癖的有追求的程序员来说, 怎么受得了呢!

react的error或者warning信息还是写得比较好的, 从上面我们可以看出原因是我们在一个unmounted的component上调用setState方法。分析业务代码, 发现是某个弹窗component需要从server加载数据, 有时候网络慢, 还没有加载出来用户就把弹窗关了, 所以对应的component变成了unmounted, 等到fetch请求成功之后, 再调用setState就warning了。

为了方便分析问题, 我把问题简化了, 同时为了用户直接能在浏览器打开看到效果, 而不用nodejs、npm、babel、webpack、react等一堆东西install半天, 我直接引用了react cdn上的文件。代码如下:

<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>

<body>
<div id="root"></div>
<script type="text/babel">

class Big extends React.Component {
constructor() {
super();
this.state = {
small: true
}
}
closeSmall = () => {
console.log('closeSmall');
this.setState({ small: false });
}
render() {
return <div>
<h1>hello from Big Component </h1>
<h2 onClick={this.closeSmall}>close small </h2>
{this.state.small ? <Small /> : null}
</div>
}
}
class Small extends React.Component {
constructor() {
super();
this.state = {
data: 'init data'
}
}
componentDidMount() {
console.log('componentDidMount');
setTimeout(() => {
console.log('fetch data from server succeed...')
console.log(`this._isMounted: ${this._isMounted}`)
this.setState({ data: 'data from server' });
}, 5000)
}
render() {
return <div>
<h1>hello from Small Component ...  </h1>
data: {this.state.data}
</div>
}
}

ReactDOM.render(
<Big />,
document.getElementById('root')
);

</script>
</body>

</html>

代码里面用setTimeout模拟了从server获取数据, 大家如果在5s内点击close small, 就可以重现这个问题。

问题的解决方法很自然地想到,如果可以在setState之前检查一下this component是否还是mounted状态就可以了。查react的文档,发现原来之前确实是有isMounted()这个方法的, 不过已经不推荐使用了, 因为isMounted is an Antipattern

第一种解决方法就是自己模拟实现isMounted这个方法, 虽然已经被贴上Antipattern的标签, 但是有些时候用这种方法还是比较方便的。代码如下:

<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>

<body>
<div id="root"></div>
<script type="text/babel">

class Big extends React.Component {
constructor() {
super();
this.state = {
small: true
}
}
closeSmall = () => {
console.log('closeSmall');
this.setState({ small: false });
}
render() {
return <div>
<h1>hello from Big Component </h1>
<h2 onClick={this.closeSmall}>close small </h2>
{this.state.small ? <Small /> : null}
</div>
}
}
class Small extends React.Component {
constructor() {
super();
this.state = {
data: 'init data'
}
this._isMounted = false;
}
componentDidMount() {
this._isMounted = true;
console.log('componentDidMount');
setTimeout(() => {
console.log('fetch data from server succeed...')
console.log(`this._isMounted: ${this._isMounted}`)
if (this._isMounted) {
this.setState({ data: 'data from server' });
}
}, 5000)
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
return <div>
<h1>hello from Small Component ...  </h1>
data: {this.state.data}
</div>
}
}

ReactDOM.render(
<Big />,
document.getElementById('root')
);

</script>
</body>

</html>

对于callback现在已经有更好的解决方案, 伟大的Promise!如果这个promise能在componentWillUnmount()的时候cancel掉就完美了。可惜google之后发现官方Promise实现目前并不支持cancel!看这里, 还有这里,所以除非你使用第三方Promise库, 比如据说性能比原生还好的Bluebird

当然有些时候没必要搞这么复杂, facebook的文档给了一个简易的cancelable的Promise。最好代码如下:

<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>

<body>
<div id="root"></div>
<script type="text/babel">

class Big extends React.Component {
constructor() {
super();
this.state = {
small: true
}
}
closeSmall = () => {
console.log('closeSmall');
this.setState({ small: false });
}
render() {
return <div>
<h1>hello from Component </h1>
<h2 onClick={this.closeSmall}>close small </h2>
{this.state.small ? <Small /> : null}
</div>
}
}
class Small extends React.Component {
constructor() {
super();
this.state = {
data: 'init data'
}
}
componentDidMount() {
console.log('componentDidMount');
this.cancelablePromise = makeCancelable(new Promise((resolve, reject) => {
setTimeout(() => {
console.log('fetch data from server succeed...')
resolve('data from server');
}, 5000)
}))

this.cancelablePromise
.promise
.then(data => {
console.log('resolved: ', data, this.state);
this.setState({ data });
})
.catch(reason => console.log(reason, ' isCanceled', reason.isCanceled));

}
componentWillUnmount() {
this.cancelablePromise.cancel();// Cancel the promise
}
render() {
return <div>
<h1>hello from Small Component ...  </h1>
data: {this.state.data}
</div>
}
}

const makeCancelable = (promise) => {
let hasCanceled_ = false;

const wrappedPromise = new Promise((resolve, reject) => {
promise.then((val) =>
hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)
);
promise.catch((error) =>
hasCanceled_ ? reject({ isCanceled: true }) : reject(error)
);
});

return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
ReactDOM.render(
<Big />,
document.getElementById('root')
);

</script>
</body>

</html>

well, it's ok now!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息