You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
constProfilePage=React.lazy(()=>import('./ProfilePage'));// Lazy-loaded// Show a spinner while the profile is loading<Suspensefallback={<Spinner/>}><ProfilePage/></Suspense>;
Suspense for Data Fetching 是一个新特性,也让你能使用 <Suspense> 来 ”等待” 其它的,包括数据。该页主要聚焦于数据请求这种使用场景,其实,它也能等待图片、脚本和其它的异步工作。
确切的说,什么是 Suspense?
Suspense 让你的组件在渲染前 ”等待” 某件事。在下面的例子中,2 个组件等待异步 API 来请求数据:
constresource=fetchProfileData();functionProfilePage(){return(<Suspensefallback={<h1>Loading profile...</h1>}><ProfileDetails/><Suspensefallback={<h1>Loading posts...</h1>}><ProfileTimeline/></Suspense></Suspense>);}functionProfileDetails(){// Try to read user info, although it might not have loaded yetconstuser=resource.user.read();return<h1>{user.name}</h1>;}functionProfileTimeline(){// Try to read posts, although they might not have loaded yetconstposts=resource.posts.read();return(<ul>{posts.map(post=>(<likey={post.id}>{post.text}</li>))}</ul>);}
除非你有解决方案来帮助你处理阻止 waterfalls 问题,我们建议在渲染前同意或者执行请求 APIs。一个具体例子,你可以看到 Relay Suspense API 如何执行预加载。过去,针对此问题我们的发布并没有保持一致性。Suspense for Data Fetching 仍然具有实验性,随着我们从生产使用中了解更多和更好理解这个问题,我们的建议也会改变。
// Kick off fetching as early as possibleconstpromise=fetchProfileData();functionProfilePage(){const[user,setUser]=useState(null);const[posts,setPosts]=useState(null);useEffect(()=>{promise.then(data=>{setUser(data.user);setPosts(data.posts);});},[]);if(user===null){return<p>Loading profile...</p>;}return(<><h1>{user.name}</h1><ProfileTimelineposts={posts}/></>);}// The child doesn't trigger fetching anymorefunctionProfileTimeline({ posts }){if(posts===null){return<h2>Loading posts...</h2>;}return(<ul>{posts.map(post=>(<likey={post.id}>{post.text}</li>))}</ul>);}
// This is not a Promise. It's a special object from our Suspense integration.constresource=fetchProfileData();functionProfilePage(){return(<Suspensefallback={<h1>Loading profile...</h1>}><ProfileDetails/><Suspensefallback={<h1>Loading posts...</h1>}><ProfileTimeline/></Suspense></Suspense>);}functionProfileDetails(){// Try to read user info, although it might not have loaded yetconstuser=resource.user.read();return<h1>{user.name}</h1>;}functionProfileTimeline(){// Try to read posts, although they might not have loaded yetconstposts=resource.posts.read();return(<ul>{posts.map(post=>(<likey={post.id}>{post.text}</li>))}</ul>);}
// Start fetching early!constresource=fetchProfileData();// ...functionProfileDetails(){// Try to read user infoconstuser=resource.user.read();return<h1>{user.name}</h1>;}
// First fetch: as soon as possibleconstinitialResource=fetchProfileData(0);functionApp(){const[resource,setResource]=useState(initialResource);return(<><buttononClick={()=>{constnextUserId=getNextId(resource.userId);// Next fetch: when the user clickssetResource(fetchProfileData(nextUserId));}}>
Next
</button><ProfilePageresource={resource}/></>);}
functionProfilePage({ id }){const[user,setUser]=useState(null);useEffect(()=>{fetchUser(id).then(u=>setUser(u));},[id]);if(user===null){return<p>Loading profile...</p>;}return(<><h1>{user.name}</h1><ProfileTimelineid={id}/></>);}functionProfileTimeline({ id }){const[posts,setPosts]=useState(null);useEffect(()=>{fetchPosts(id).then(p=>setPosts(p));},[id]);if(posts===null){return<h2>Loading posts...</h2>;}return(<ul>{posts.map(post=>(<likey={post.id}>{post.text}</li>))}</ul>);}
classProfilePageextendsReact.Component{state={user: null};componentDidMount(){this.fetchData(this.props.id);}componentDidUpdate(prevProps){if(prevProps.id!==this.props.id){this.fetchData(this.props.id);}}asyncfetchData(id){constuser=awaitfetchUser(id);this.setState({ user });}render(){const{ id }=this.props;const{ user }=this.state;if(user===null){return<p>Loading profile...</p>;}return(<><h1>{user.name}</h1><ProfileTimelineid={id}/></>);}}classProfileTimelineextendsReact.Component{state={posts: null};componentDidMount(){this.fetchData(this.props.id);}componentDidUpdate(prevProps){if(prevProps.id!==this.props.id){this.fetchData(this.props.id);}}asyncfetchData(id){constposts=awaitfetchPosts(id);this.setState({ posts });}render(){const{ posts }=this.state;if(posts===null){return<h2>Loading posts...</h2>;}return(<ul>{posts.map(post=>(<likey={post.id}>{post.text}</li>))}</ul>);}}
<><buttononClick={()=>{constnextUserId=getNextId(resource.userId);setResource(fetchProfileData(nextUserId));}}>
Next
</button><ProfilePageresource={resource}/></>
// Error boundaries currently have to be classes.classErrorBoundaryextendsReact.Component{state={hasError: false,error: null};staticgetDerivedStateFromError(error){return{hasError: true,
error
};}render(){if(this.state.hasError){returnthis.props.fallback;}returnthis.props.children;}}
然后把它放在树中来捕获错误:
functionProfilePage(){return(<Suspensefallback={<h1>Loading profile...</h1>}><ProfileDetails/><ErrorBoundaryfallback={<h2>Could not fetch posts.</h2>}><Suspensefallback={<h1>Loading posts...</h1>}><ProfileTimeline/></Suspense></ErrorBoundary></Suspense>);}
Suspense for Data Fetching
React
v16.6 中添加了Suspense
组件,让你 ”等待” 加载一些代码和指定一个在等待期间中显示的加载状态(像一个 spinner)。Suspense for Data Fetching
是一个新特性,也让你能使用<Suspense>
来 ”等待” 其它的,包括数据。该页主要聚焦于数据请求这种使用场景,其实,它也能等待图片、脚本和其它的异步工作。确切的说,什么是
Suspense
?Suspense
让你的组件在渲染前 ”等待” 某件事。在下面的例子中,2 个组件等待异步 API 来请求数据:在 CodeSandbox 中尝试
上面的例子是一个预告,不理解也不要紧。现在将讲解更多它是如何工作的。记住
Suspense
更是一种机制,在上面的例子中,特殊的 API 像 fetchProfileData() 或者 resource.posts.read 是不重要的。如果你很好奇,你可以在 sandbox 的例子 中找到对应的定义。Suspense
不是一种数据请求库,而是数据请求库告诉React
组件需要的数据目前没有准备好的一种机制。然后React
等到数据准备好后更新UI
。在 Facebook,我们使用集成了Suspense
的Relay
。我们希望其它库比如像Apollo
也能提供相似的集成。长期以来,我们想让
Suspense
成为组件中读取异步数据的方式 - 无关数据来源。Suspense
不是什么?Suspense
和现有解决方案有很大不同,首次读到这些可能让你困惑。下面阐述最常见的部分:Suspense
来 ”替换” 请求或者Relay
。但是你可以使用集成了Suspense
的库(例如,新的Relay
APIs)React
组件中。Suspense
让你做什么所以
Suspense
的重点是什么呢?我们回答一下:React
。如果一个数据请求库实现了对Suspense
的支持,那么在React
组件中使用Suspense
会感觉非常自然。Suspense
更像同步读取数据 - 好像它已经被加载。Suspense
实践在 Facebook,到目前为止我们仅仅在生产环境中使用了集成了
Suspense
的Relay
。如果你正找一个实践指南,请检出Relay
指南!该指南示范了在生产中工作的模式。该页中的代码例子使用了 “fake” API 实现而非
Relay
。如果你不熟悉GraphQL
,那么这个例子使得理解变的简单,但并不是使用Suspense
来构建应用的 “正确方式”。该页更多是概念性的,介绍了Suspense
以某种方式工作的原因和它解决的问题。如果不使用
Relay
怎么办如果不使用
Relay
,你必须等一段时间才能在项目中尝试Suspense
。距今为止,Relay
是我们在生产上测试过的唯一实现。在未来几个月里,许多库会有关于Suspense
APIs 的不同实现。如果你更喜欢在事物稳定时学习,你可以现在忽略它,等到Suspense
生态更成熟时再学习。如果你喜欢,也可以为数据请求库写集成。
对于库作者
我们期望看到社区中其它库的实验。对于数据请求库作者来说,有 1 件重要的事情需要注意。
尽管技术上可行,
Suspense
不是在组件渲染时作为开始请求数据的方式,而是让组件表示它们正在等待已经请求的数据。使用Concurrent Mode
和Suspense
构建友好用户体验 解释了它的重要性和如何在实际中实现这种模式除非你有解决方案来帮助你处理阻止 waterfalls 问题,我们建议在渲染前同意或者执行请求 APIs。一个具体例子,你可以看到 Relay Suspense API 如何执行预加载。过去,针对此问题我们的发布并没有保持一致性。
Suspense for Data Fetching
仍然具有实验性,随着我们从生产使用中了解更多和更好理解这个问题,我们的建议也会改变。传统方式和
Suspense
的对比我们介绍
Suspense
没有提及其它流行的数据请求方式。这很难看出来Suspense
解决了什么问题和为什么这些问题值得解决以及Suspense
和已存在的方式有何不同。接下来,我们将看下包括
Suspense
在内的三种方式:Fetch-on-render
(例如,在 _ useEffect_ 中请求):开始渲染组件。每个组件都在它们的副作用和生命周期函数中触发数据请求。这种方式经常导致 “waterfalls”。Fetch-then-render
(例如,不使用Suspense
的Relay
):尽早请求下一屏需要所有的数据。当数据准备好后,渲染新视图。直到数据拿到时才能做其它事情。Render-as-you-fetch
(例如,使用Suspense
的Relay
):尽早请求下一屏必需的所有数据,在拿到请求响应前,立即开始渲染新视图。直到数据准备好后,React
重新尝试渲染组件。为了比较这三种方式,我们将利用其各自实现一个介绍页。
解决方案一:
Fetch-on-Render
(不使用Suspense
)现在,在
React
应用中请求数据一种通用的方式就是使用 effect:我们称呼这种方式为
Fetch-on-Render
,因为直到组件在屏幕上渲染完毕才会开始请求数据。这样会导致一个叫做waterfall
的问题。考虑一下 和 组件:
在 CodeSandbox 中尝试
运行代码观察控制台日志,发现顺序是:
如果请求用户详情花费 3s,那么将在 3s 后请求发布!这就是 “waterfall”:不相关的顺序应该被并行。
在代码中渲染之后请求数据通常会出现
Waterfalls
问题。它们应该被解决,不过随着业务的增长,很多人更喜欢预防这个问题。方式二:
Fetch-Then-Render
(不使用Suspense
)库通过提供一个集中的方式进行数据请求来阻止
waterfalls
。例如,Relay
解决这个问题,通过移动组件需要的数据到静态分析碎片,之后组合成一个请求。该页,假设不了解
Relay
,在这个例子中我们不必用它,使用合并数据请求方法来替代:在这个例子中, 等待并行开始的 2 个请求
在 CodeSandbox 中尝试
现在顺序变成现在这样:
我们解决了之前的网络 “waterfall”,但是意外的引入了其它问题。我们在 _ fetchProfileData_ 里面使用 Promise.all() 来等待所有数据,直到发布数据也被请求完成后才会渲染介绍详情,可是我们不得不一直等待 2 个接口的返回。
当然,在这个特殊的例子中修复是可能的。可以移除 Promise.all() 调用,分别等待 2 个 Promises 的返回结果。然而,这种方式会随着数据复杂和组件树增长而变得更加困难。当数据树中的部分可能丢失或者过期时,写一个可靠的组件变得很难。因此为一个新视图请求所有数据然后渲染经常是一个更加实践的选项。
方式三:
Render-as-You-Fetch
(使用Suspense
)在上面一种方式中,在调用 setState 之前来请求数据
使用
Suspense
,仍然首先请求数据,但是我们交换后 2 步:使用
Suspense
,无需在渲染前等待响应。事实上,在开始网络请求后马上开始渲染。在 CodeSandbox 中尝试
在视图上渲染
<ProfilePage>
后发生了以下事件:fetchProfileData()
中开始请求。返回一个特别的 “resource” 来取代 Promise。真实例子中,可能是一个集成了Suspense
数据请求库,像Relay
。React
尝试渲染<ProfilePage>
。返回孩子节点<ProfileDetails>
和<ProfileTimeline>
。React
尝试渲染<ProfileDetails>
,调用resource.user.read()
。数据请求没有完成,所以该组件被挂起。React
跳过它,然后尝试树中的其它组件。React
尝试渲染<ProfileTimeline>
。调用resource.posts.read()
。同时,也没有数据被准备好,因此这个组件也被挂起。React
也跳过它,然后尝试树中的其它组件。<ProfileDetails>
被挂起了,所以React
显示在树中它上面最近<Suspense>
的fallback
:<h1>Loading profile...</h1>
。resource 对象代表目前没有的数据,不过最后会加载。当调用 read() 时,要不得到数据要不组件被挂起。
随着更多数据流产出,React 将会尝试重新渲染,每次进度都会更近一步。 当 resource.user 请求完成,
<ProfileDetails>
组件会被成功渲染,不再需要<h1>Loading profile...</h1>
fallback。最后,我们将获得所有数据,屏幕上不再有 fallback。有一个有意思的地方是使用 GraphQL ,在一个单独请求中收集所有数据需求,流化响应让我们更快展示更多内容。因为
render-as-we-fetch
(和渲染后相反),如果接口返回 user 早于 posts,将会在响应结束前解锁外部<Suspense>
。回忆一下之前的例子,fetch-then-render
这种解决方案包括waterfall
:在请求和渲染之间。Suspense
没有waterfall
这种问题你,像Relay
一样的库利用了这一点。看一下我们是如何从我们的组件里排除了
if (…)
“正在加载” 的检查。这样做不仅移除了样板代码,也简化了设计改变。例如,如果我们想要介绍详情和发布总是一起 “弹出”,我们需要删除它们之间的<Suspense>
。或者我们想要使用各自的<Suspense>
来彼此独立。Suspense
改变加载状态粒度和没有侵入性改变代码来改变它们的顺序。尽早开始请求
如果你正工作于一个数据请求库,你一定不想错过
Render-as-You-Fetch
。我们在渲染前开始请求数据。看下下面的代码示例:在 CodeSandbox 中尝试
在这个例子中 read() 调用没有开始请求。它仅仅在请求结束时才读取数据。这个不同在使用
Suspense
来创建应用是非常关键的。我们不想直到组件开始渲染时才延迟加载数据。作为一个数据请求库的作者,你得实现这个使得开始请求时得到 resource 对象。该页中的每个例子使用我们的 ”fake API” 模拟这个。你可能会反对在顶层请求,像这个例子一样觉着是不切合实际的。如果导航到另一个介绍页会做什么呢?我们想要基于 props 来请求。答案是 我们想要在事件处理中开始请求。下面是一个简单的用户页面导航的例子:
在 CodeSandbox 中尝试
使用这种方式,我们能 并行请求代码和数据。当在页面之间切换时,无需等待页面的代码开始加载它所需的数据。我们可以同时加载代码和数据(在链接点击期间),获得更好的用户体验。
还有一个问题就是我们怎么知道在下一屏渲染前渲染什么。有很多方式来解决这个问题(例如,通过路由解决方案集成数据请求)。如果你工作于数据请求数据,使用
Concurrent
模式和Suspense
提升用户体验深入解读如何做和为什么如此重要。我们需要弄清楚的事情
Suspense
机制很弹性,没有很多约束。业务代码需要更加约束确保没有waterfalls
,但是有不同方式来确保这个。我们正在探索的一些问题包括:waterfalls
?Suspense
数据还有其它类似于组合 GraphQL 查询的同等方式吗?Relay
有针对上面问题的答案,相信肯定还有其它解决方案,我们很高兴能看到React
社区能提出一些新的想法。Suspense
和 竞态条件代码运行的顺序不正确可能导致 竞态条件。在 Hook useEffect 或者在类组件生命周期函数像 componentDidUpdate 中请求数据经常导致这个。
Suspense
对这个也有帮助 - 让我们看下。为了演示这个问题,将添加顶层
<App>
组件来渲染<ProfilePage>
和一个按钮让我们在不同介绍页之间切换:对比一下不同数据请求策略如何处理。
使用
useEffect
导致的 竞态条件首先,我们尝试上面 ”在 effect 中请求” 的例子版本。修改它从
<ProfilePage>
的 props 中传递id
参数给 fetchUser(id) 和 fetchPosts(id):在 CodeSandbox 中尝试
把 effect 的依赖从 [] 改变成 [id] - 因为我们想要当 id 改变时 effect 重新运行。否则,不能重新请求新数据。
运行代码,第一次看起来没有问题。如果在 “fake API” 实现中随机延迟时间和快速点击 Next 按钮,我们将看到控制台日志有些错误。在切换介绍页到另一个 ID 时上一个介绍的请求可能有时才回来 - 在这种场景中可能用不同 ID 的过期响应来覆盖新的状态
这个问题可能被修复(你可能使用 effect 清理 函数来忽略或者取消过期请求),但是会变得不直观和难于调试。
使用 componentDidUpdate 导致的 竞态条件
有人想这个问题可能仅存在于 useEffect 或者 Hooks 中。
如果我们把代码放到类中或者使用像 async / await 方便的语法,这样会解决问题吗?
让我们试下:
在 CodeSandbox 中尝试
以上代码不易阅读。
不幸的是,使用类和 async / await 语法仍然没有解决问题。同样的原因,这个版本也有 竞态条件 问题。
问题
React
组件有它们自己的生命周期。在任何时间点,它们能接收到 props 或者更新状态。然而,每一个异步请求也有它自己的生命周期。当我们开始时它开始,当我们得到响应时结束。难点在于相互影响的多进程及时同步讨论。使用
Suspense
来解决 竞态条件 问题重写上面的例子,但是只使用
Suspense
:在 CodeSandbox 中尝试
在上面的
Suspense
例子中,我们只有一个 resource,因此在顶层变量中保存它。现在我们有了多个 resources,我们把它移动到<App>
组件的 state 中:当我们点击 ”Next” 时,
<App>
组件为下一个介绍开始下一个请求,传输给<ProfilePage>
组件一个对象:我们不等待响应来设置状态。在开始请求后立即设置 state(开始渲染)。一旦我们有更多的数据,
React
将会在<Suspense>
组件内部填入内容。Suspense
版本不像较早的例子,代码变得可读,也没有 竞态条件 问题。你可能想知道原因。答案是在Suspense
版本中,我们不必在我们代码中考虑时间。有 竞态条件 的代码需要在随后准确的时间中设置状态,否则它有可能出错。但是使用Suspense
,我们马上设置状态 - 所以混乱变得很难。错误处理
当我们使用 Promises 写代码时,我们使用 catch() 来处理错误。考虑不等待 Promises 就开始渲染,使用
Suspense
如何处理错误的?使用
Suspense
,处理请求错误和处理渲染错误是一样的方式 - 你能在下面的组件里任意处渲染一个 错误边界 来处理错误。首先,在项目中定义一个 边界错误 组件:
然后把它放在树中来捕获错误:
在 CodeSandbox 中尝试
它能同时捕获渲染和
Suspense data fetching
的错误。可以按照我们的需求来定义错误边界,但是最好设计它们的位置。下一步
现在,我们已经概述了
Suspense for Data Fetching
的基础。已经能够更好理解Suspense
为什么生效和它怎样在数据请求中使用。Suspense
回答了这些问题,同时也有自身的新问题:为了回答以上问题,可以查看 Concurrent UI Patterns 这部分。
The text was updated successfully, but these errors were encountered: