首页 > web前端 > css教程 > 悬念:加载数据时学到的教训

悬念:加载数据时学到的教训

尊渡假赌尊渡假赌尊渡假赌
发布: 2025-03-18 10:18:15
原创
986 人浏览过

React Suspense: 从数据加载中汲取的经验教训

React Suspense: Lessons Learned While Loading Data

Suspense是React即将推出的一个功能,它有助于协调异步操作(例如数据加载),使您可以轻松防止UI中的状态不一致。我将对这到底意味着什么进行更详细的解释,并简要介绍Suspense,然后介绍一个比较现实的用例,并介绍一些经验教训。

我介绍的功能仍处于alpha阶段,绝不能用于生产环境。这篇文章是为那些想要抢先体验即将推出的功能并了解未来发展方向的人准备的。

Suspense入门

应用程序开发中最具挑战性的部分之一是协调应用程序状态和数据加载方式。状态更改通常会在多个位置触发新的数据加载。通常,每条数据都会有自己的加载UI(例如“旋转器”),大致位于数据在应用程序中的位置。数据加载的异步特性意味着这些请求可以以任何顺序返回。结果,您的应用程序不仅会出现许多不同的旋转器出现和消失,更糟糕的是,您的应用程序可能会显示不一致的数据。如果您的三个数据加载中有两个已完成,那么您将看到一个加载旋转器位于第三个位置的顶部,仍然显示旧的、现在已过时的数据。

我知道这太多了。如果您觉得其中任何内容令人费解,您可能对之前我撰写的一篇关于Suspense的文章感兴趣。该文章更详细地介绍了Suspense是什么以及它实现了什么。请注意,其中一些小细节现在已经过时了,即useTransition钩子不再接受timeoutMs值,而是无限期等待。

现在,让我们快速浏览一下细节,然后进入一个具体的用例,其中有一些潜伏的陷阱。

Suspense的工作原理

幸运的是,React团队足够聪明,没有将这些努力限制在仅仅加载数据上。Suspense通过低级基元工作,您可以将其应用于几乎任何事物。让我们快速浏览一下这些基元。

首先是<suspense></suspense>边界,它接受一个fallback属性:

<suspense fallback="{<Fallback"></suspense>}>
登录后复制
登录后复制

每当此组件下的任何子组件挂起时,它都会呈现fallback。无论有多少子组件挂起,无论出于何种原因,显示的都是fallback。这是React确保UI一致的一种方式——在所有内容准备就绪之前,它不会呈现任何内容。

但是,在内容最初呈现之后,用户更改状态并加载新数据会怎样呢?我们当然不希望我们现有的UI消失并显示我们的fallback;那将是糟糕的用户体验。相反,我们可能希望显示一个加载旋转器,直到所有数据准备就绪,然后才显示新的UI。

useTransition钩子实现了这一点。此钩子返回一个函数和一个布尔值。我们调用该函数并包装我们的状态更改。现在事情变得有趣了。React尝试应用我们的状态更改。如果任何内容挂起,React会将该布尔值设置为true,然后等待挂起结束。完成后,它将尝试再次应用状态更改。也许这次它会成功,或者也许其他内容会挂起。无论如何,布尔标志都会保持为true,直到所有内容都准备就绪,并且只有在那时,状态更改才会完成并反映在UI中。

最后,我们如何挂起?我们通过抛出一个promise来挂起。如果请求数据并且我们需要获取,那么我们获取——并抛出一个与该获取相关的promise。这种低级别的挂起机制意味着我们可以将其用于任何事物。用于延迟加载组件的React.lazy实用程序已经与Suspense一起工作,我之前已经写过关于使用Suspense在显示UI之前等待图像加载以防止内容移动的文章。

别担心,我们会讨论所有这些。

我们正在构建什么

我们将构建一些与其他类似文章中的示例略有不同的事物。请记住,Suspense仍在alpha阶段,因此您最喜欢的加载数据实用程序可能还没有Suspense支持。但这并不意味着我们不能伪造一些东西并了解Suspense的工作原理。

让我们构建一个无限加载列表,该列表显示一些数据,并结合一些基于Suspense的预加载图像。我们将显示我们的数据,以及一个加载更多数据的按钮。当数据呈现时,我们将预加载关联的图像,并在准备就绪之前挂起。

此用例基于我在我的副项目中完成的实际工作(再次,不要在生产环境中使用Suspense——但副项目是允许的)。我当时正在使用我自己的GraphQL客户端,这篇文章的动机是我遇到的一些困难。为了简化操作并专注于Suspense本身,而不是任何单个数据加载实用程序,我们将只伪造数据加载。

开始构建!

这是我们初始尝试的沙箱。我们将使用它来逐步讲解所有内容,因此现在不必急于理解所有代码。

我们的根App组件呈现一个像这样的Suspense边界:

<suspense fallback="{<Fallback"></suspense>}>
登录后复制
登录后复制

每当任何内容挂起(除非状态更改发生在useTransition调用中),fallback都会呈现。为了使事情更容易理解,我使这个Fallback组件将整个UI变成粉红色,这样就很难错过;我们的目标是理解Suspense,而不是构建高质量的UI。

我们正在DataList组件内加载当前的数据块:

const newData = useQuery(param);
登录后复制

我们的useQuery钩子被硬编码为返回伪造数据,包括模拟网络请求的超时。它处理缓存结果,如果数据尚未缓存,则抛出一个promise。

我们(至少目前)将状态保存在我们正在显示的主数据列表中:

const [data, setData] = useState([]);
登录后复制

当新的数据从我们的钩子传入时,我们将其附加到我们的主列表中:

useEffect(() => {
  setData((d) => d.concat(newData));
}, [newData]);
登录后复制
登录后复制

最后,当用户需要更多数据时,他们单击该按钮,该按钮会调用此函数:

function loadMore() {
  startTransition(() => {
    setParam((x) => x   1);
  });
}
登录后复制

最后,请注意,我正在使用SuspenseImg组件来处理我正在与每条数据一起显示的图像的预加载。只显示五张随机图像,但我添加了一个查询字符串以确保为我们遇到的每条新数据进行新的加载。

总结

为了总结我们目前所处的位置,我们有一个加载当前数据的钩子。该钩子遵守Suspense机制,并在加载发生时抛出一个promise。每当该数据更改时,正在运行的项目总列表都会更新并附加新项目。这发生在useEffect中。每个项目都呈现一个图像,我们使用SuspenseImg组件来预加载图像,并在准备就绪之前挂起。如果您好奇某些代码的工作原理,请查看我之前关于使用Suspense预加载图像的文章。

让我们测试一下

如果一切正常,这将是一篇非常无聊的博客文章,别担心,它不正常。请注意,在初始加载时,粉红色的fallback屏幕会显示然后快速隐藏,但随后会重新显示。

当我们单击加载更多数据的按钮时,我们会看到内联加载指示器(由useTransition钩子控制)翻转为true。然后我们看到它翻转为false,然后我们的原始粉红色fallback显示。我们期望在初始加载后不再看到那个粉红色屏幕;内联加载指示器应该显示直到所有内容都准备就绪。发生了什么?

问题

它一直隐藏在显眼之处:

useEffect(() => {
  setData((d) => d.concat(newData));
}, [newData]);
登录后复制
登录后复制

useEffect在状态更改完成时运行,即状态更改已完成挂起,并已应用于DOM。那部分,“已完成挂起”是这里的关键。如果我们愿意,我们可以在此处设置状态,但是如果该状态更改再次挂起,则这是一个全新的挂起。这就是为什么我们在初始加载以及随后数据加载完成后看到粉红色闪烁的原因。在这两种情况下,数据加载都已完成,然后我们在一个效果中设置状态,这导致新数据实际呈现并再次挂起,因为图像预加载。

那么,我们如何解决这个问题呢?在一个层面上,解决方案很简单:停止在效果中设置状态。但这说起来容易做起来难。我们如何在不使用效果的情况下更新正在运行的条目列表以附加新的结果?您可能认为我们可以使用ref来跟踪事物。

不幸的是,Suspense带来了一些关于ref的新规则,即我们不能在渲染内部设置ref。如果您想知道为什么,请记住Suspense完全是关于React尝试运行渲染,看到promise被抛出,然后在中途丢弃该渲染。如果我们在渲染被取消和丢弃之前更改了ref,则ref仍然具有该更改,但无效的值。渲染函数需要是纯的,没有副作用。这始终是React的一条规则,但现在它更重要了。

重新思考我们的数据加载

这是解决方案,我们将逐段讲解。

首先,与其将我们的主数据列表存储在状态中,不如做些不同的事情:让我们存储我们正在查看的页面列表。我们可以将最近的页面存储在ref中(尽管我们不会在渲染中写入它),并将所有当前加载的页面的数组存储在状态中。

const currentPage = useRef(0);
const [pages, setPages] = useState([currentPage.current]);
登录后复制

为了加载更多数据,我们将相应地更新:

function loadMore() {
  startTransition(() => {
    currentPage.current = currentPage.current   1;
    setPages((pages) => pages.concat(currentPage.current));
  });
}
登录后复制

然而,棘手的部分是如何将这些页码转换为实际数据。我们当然不能做的是循环遍历这些页面并调用我们的useQuery钩子;钩子不能在循环中调用。我们需要一个新的、非基于钩子的数据API。根据我在过去的Suspense演示中看到的非常非官方的约定,我将此方法命名为read()。它不会是一个钩子。如果数据被缓存,它会返回请求的数据,否则会抛出一个promise。对于我们的伪造数据加载钩子,没有必要进行任何真正的更改;我只是简单地复制粘贴了钩子,然后将其重命名。但是对于实际的数据加载实用程序库,作者可能需要做一些工作才能将这两个选项都作为其公共API的一部分公开。在我前面提到的GraphQL客户端中,确实同时存在useSuspenseQuery钩子和客户端对象上的read()方法。

有了这个新的read()方法,我们代码的最后部分就微不足道了:

const data = pages.flatMap((page) => read(page));
登录后复制

我们获取每个页面,并使用我们的read()方法请求相应的数据。如果任何页面未被缓存(实际上应该只有列表中的最后一个页面),则会抛出一个promise,React会为我们挂起。当promise解析时,React会再次尝试之前的状态更改,这段代码也会再次运行。

不要让flatMap调用迷惑你。它与map做的事情完全相同,只是它获取新数组中的每个结果,如果它本身是一个数组,则将其“展平”。

结果

通过这些更改,当我们开始时,一切都会按预期工作。我们的粉红色加载屏幕在初始加载时显示一次,然后在后续加载中,内联加载状态显示直到所有内容都准备就绪。

结束语

Suspense是对React的令人兴奋的更新。它仍处于alpha阶段,因此不要尝试将其用于任何重要的事情。但是,如果您是那种喜欢抢先体验即将推出的内容的开发人员,那么我希望这篇文章为您提供了一些有用的背景信息和信息,这些信息在发布时会有用。

以上是悬念:加载数据时学到的教训的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板