竞态
下面是一个典型的在class组件里发请求的例子:
class Article extends Component {
state = {
article: null
};
componentDidMount() {
this.fetchData(this.props.id);
}
async fetchData(id) {
const article = await API.fetchArticle(id);
this.setState({ article });
}
// ...
}
你很可能已经知道,上面的代码埋伏了一些问题。它并没有处理更新的情况。
class Article extends Component {
state = {
article: null
};
componentDidMount() {
this.fetchData(this.props.id);
}
componentDidUpdate(prevProps) { // look here
if (prevProps.id !== this.props.id) {
this.fetchData(this.props.id);
}
}
async fetchData(id) {
const article = await API.fetchArticle(id);
this.setState({ article });
}
// ...
}
这显然好多了!但依旧有问题。有问题的原因是请求结果返回的顺序不能保证一致。
比如我先请求 {id: 10}
,然后更新到{id: 20}
,但{id: 20}
的请求更先返回。
请求更早但返回更晚的情况会错误地覆盖状态值。
这种被叫做竞态
这在混合了async
/ await
(假设在等待结果返回)和自顶向下数据流的代码中非常典型
props
和state
可能会在async
函数调用过程中发生改变。
Effects
并没有神奇地解决这个问题,尽管它会警告你如果你直接传了一个async
函数给effect
。
(react团队
会改善这个警告来更好地解释你可能会遇到的这些问题。)
如果你使用的异步方式支持取消,那太棒了。你可以直接在清除函数中取消异步请求。
或者,最简单的权宜之计是用一个布尔值来跟踪它:
function Article({ id }) {
const [article, setArticle] = useState(null);
useEffect(() => {
let didCancel = false;
async function fetchData() {
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}
fetchData();
return () => {
didCancel = true;
};
}, [id]);
// ...
}
这篇文章讨论了更多关于如何处理错误和加载状态,以及抽离逻辑到自定义的Hook
。
提高水准
在class
组件生命周期的思维模型中,副作用的行为和渲染输出是不同的。UI
渲染是被props
和state
驱动的,并且能确保步调一致,但副作用并不是这样。这是一类常见问题的来源。
而在useEffect
的思维模型中,默认都是同步的。
副作用变成了React数据流
的一部分。对于每一个useEffect
调用,一旦你处理正确,你的组件能够更好地处理边缘情况。
然而,用好useEffect
的前期学习成本更高。这可能让人气恼。用同步的代码去处理边缘情况天然就比触发一次不用和渲染结果步调一致的副作用更难。
目前为止,useEffect
主要用于数据请求。但是数据请求准确说并不是一个同步问题。因为我们的依赖经常是[]
所以这一点尤其明显。那我们究竟在同步什么?
长远来看, Suspense用于数据请求 会允许第三方库通过第一等的途径告诉
React`暂停渲染直到某些异步事物(任何东西:代码,数据,图片)已经准备就绪。
当Suspense
逐渐地覆盖到更多的数据请求使用场景,我预料useEffect
会退居幕后作为一个强大的工具,用于同步props
和state
到某些副作用。
不像数据请求,它可以很好地处理这些场景因为它就是为此而设计的。
不过在那之前,自定义的Hooks
比如这儿提到的是复用数据请求逻辑很好的方式。