React 的实验性 Suspense API 将在数据获取期间摇摆 Fallback UI

Avatar of Kingsley Silas
Kingsley Silas

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费信用额度!

如今构建的大多数 Web 应用程序都从 API 接收数据。当 获取 该数据时,我们必须考虑某些情况,在这种情况下,数据可能尚未接收。也许连接丢失了。也许端点已更改。谁知道呢。无论问题是什么,最终都是最终用户在前端得到一大堆空洞的东西。

所以我们应该对此进行说明!

处理此问题的常用方法是在应用程序中使用类似 isLoading 状态的内容。isLoading 的值取决于我们想要接收的数据。例如,它可以是一个简单的布尔值,其中返回的 true(表示我们仍在等待数据),我们显示一个加载微调器来指示应用程序正在运行。否则,我们将显示数据。

哦,天哪,不!
📷 来源:Jian Wei

虽然这并不完全糟糕,但正在努力开发 React 的那些很棒的人已经实现(并且正在继续开发)一个内置的解决方案来使用名为 Suspense 的功能来处理此问题。

Suspense 或多或少地按照其名称的意思去做

您可能已经从名称中猜到了,但 Suspense 会告诉组件在满足条件之前不要渲染。就像我们用 isLoading 讨论的那样,数据的渲染会被推迟,直到 API 获取数据并将 isLoading 设置为 false。把它想象成一个组件站在电梯里,等待到达正确的楼层再走出去。

目前,Suspense 只能用于有条件地加载使用 React.lazy() 动态渲染的组件,而无需页面重新加载。因此,假设我们有一张地图,当用户选择一个位置时,它需要一些时间来加载。我们可以用 Suspense 包裹该地图组件,并调用类似于苹果海滩球的死亡标志来显示,同时我们正在等待地图加载。然后,一旦地图加载完毕,我们就踢开球。

// Import the Map component
const Map = React.lazy(() => import('./Map'));

function AwesomeComponent() [
  return (
    // Show the <Beachball> component until the <Map> is ready
    <React.Suspense fallback={<Beachball />}>
      <div>
        <Map />
      </div>
    </React.Suspense>
  );
}

好吧。到目前为止,非常简单,我希望如此。

但如果我们想要海滩球的回退,不是针对已加载的组件,而是针对等待从 API 返回数据的情况呢?好吧,这种情况似乎非常适合 Suspense,但不幸的是,它还没有完全处理这种情况。但它会。

在此期间,我们可以使用一个名为 react-cache(包 之前 称为 simple-cache-provider)的实验性功能来演示 Suspense 在未来如何与 API 获取一起使用。

无论如何让我们将 Suspense 与 API 数据一起使用

好的,不要再“悬念”了(抱歉,忍不住)。让我们来看一个工作示例,在这个示例中,我们定义并显示一个组件作为回退,同时我们正在等待 API 返回数据。

请记住,react-cache 处于实验阶段。当我说是“实验性”的时候,我的意思就是说它真的是实验性的。甚至包描述也敦促我们不要在生产环境中使用它。

我们将构建以下内容:一个从 API 获取的用户列表。

获取源代码

好的,让我们开始吧!

首先,启动一个新项目

让我们从使用 create-react-app 生成一个新的 React 应用程序开始。

## Could be any project name
create-react-app csstricks-react-suspense

这将引导您的 React 应用程序。由于 Suspense API 仍在开发中,我们将使用不同的 React 版本。打开项目根目录中的 package.json 文件,编辑 React 和 React-DOM 版本号,并添加 simple-cache-provider 包(稍后我们将研究该包)。这就是它的样子

"dependencies": {
  "react": "16.4.0-alpha.0911da3",
  "react-dom": "16.4.0-alpha.0911da3",
  "simple-cache-provider": "0.3.0-alpha.0911da3"
}

通过运行 yarn install 来安装这些包。

在本教程中,我们将构建从 API 获取数据的功能。我们可以使用 simple-cache-provider 中的 createResource() 函数在 src/fetcher.js 文件中执行此操作

import { createResource } from 'simple-cache-provider';

const sleep = (duration) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, duration)
  })
}

const loadProfiles = createResource(async () => {
  await sleep(3000)
  const res = await fetch(`https://randomuser.me/api/?results=15`);
  return await res.json();
});

export default loadProfiles

因此,这里发生了什么。sleep() 函数会阻塞执行上下文一段特定时间,这段时间将作为参数传递。然后在 loadProfiles() 函数中调用 sleep() 函数来模拟三秒(3000 毫秒)的延迟。通过使用 createResource() 来进行 API 调用,我们要么返回已解析的值(即我们期望从 API 获取的数据),要么抛出 Promise。

接下来,我们将创建一个名为 withCache高阶组件,它可以在其包裹的组件上启用缓存。我们将在一个名为“withCache.js”的新文件中执行此操作。继续将其放置在项目的 src 目录中。

import React from 'react';
import { SimpleCache } from 'simple-cache-provider';

const withCache = (Component) => {
  return props => (
    <SimpleCache.Consumer>
      {cache => <Component cache={cache} {...props} />}
    </SimpleCache.Consumer>
  );
}

export default withCache;

此高阶组件使用来自 simple-cache-provider 包的 SimpleCache 来启用包裹组件的缓存。我保证,当我们创建下一个组件时,我们将使用它。在此期间,在 src 中创建另一个新文件,名为 Profile.js - 这是我们将遍历从 API 获取的结果的地方。

import React, { Fragment } from 'react';
import loadProfiles from './fetcher'
import withCache from './withCache'

// Just a little styling
const cardWidth = {
  width: '20rem'
}

const Profile = withCache((props) => {
  const data = loadProfiles(props.cache);
  return (
    <Fragment>
      {
        data.results.map(item => (
        <div key={item.login.uuid} className="card" style={cardWidth}>
          <div>
            <img src={item.picture.thumbnail} />
          </div>
            <p>{item.email}</p>
          </div>
        ))
      }
    </Fragment>
  )
});

export default Profile

我们这里有一个用 withCache(我们之前创建的高阶组件)包裹的 Profile 组件。现在,无论我们从 API 返回什么(已解析的 Promise),都将作为值保存到 data 变量中,该变量被定义为将传递给具有缓存的组件(props.cache)的配置文件数据的道具。

为了处理 API 返回数据之前应用程序的加载状态,我们将实现一个占位符组件,该组件将在 API 返回我们想要的数据之前进行渲染。

这就是我们希望占位符执行的操作:在 API 响应之前渲染回退 UI(可以是加载微调器、海滩球或其他任何东西),并且当 API 响应时,显示数据。我们还希望实现一个延迟(delayMs),这将有利于几乎不需要显示加载微调器的情况。例如;如果数据在不到两秒钟内返回,那么加载器可能有点傻。

占位符组件将如下所示;

const Placeholder = ({ delayMs, fallback, children }) => {
  return (
    <Timeout ms={delayMs}>
      {didTimeout => {
        return didTimeout ? fallback : children;
      }}
    </Timeout>
  );
}

delayMsfallbackchildren 将从 App 组件传递到 Placeholder 组件,我们将在稍后看到。Timeout 组件返回一个布尔值,我们可以使用它来返回回退 UI 或 Placeholder 组件的子元素(本例中的 Profile 组件)。

这是我们 App 的最终标记,将我们介绍的所有组件拼凑在一起,以及一些来自 Bootstrap 的装饰性标记来创建一个完整的页面布局。

class App extends React.Component {
  render() {
    return (
      <React.Fragment>
        // Bootstrap Containers and Jumbotron     
        <div className="App container-fluid">
          <div className="jumbotron">
            <h1>CSS-Tricks React Suspense</h1>
          </div>
          <div className="container">
            <div>
              // Placeholder contains Suspense and wraps what needs the fallback UI
              <Placeholder
                delayMs={1000}
                fallback={
                  <div className="row">
                    <div className="col-md">
                      <div className="div__loading">
                        <Loader />
                      </div>
                    </div>
                  </div>
                }
              >
                <div className="row">
                  // This is what will render once the data loads
                  <Profile />
                </div>
              </Placeholder>
            </div>
          </div>
        </div>
      </React.Fragment>
    );
  }
}

结束了

很不错,对吧?能够直接从 React 盒子里获得真正的回退 UI 支持真是太好了,无需狡猾的技巧或额外的库。鉴于 React 是用来管理状态的,而加载是需要处理的常见状态,所以这完全说得通。

请记住,尽管 Suspense 很棒(确实非常棒),但重要的是要注意,它仍然处于实验阶段,因此在生产应用程序中不实用。但是,由于今天有方法可以使用它,我们仍然可以在开发环境中随意尝试,所以尽情尝试吧!

那些一直在开发和使用 Suspense 的人一直在写下他们的想法和经验。以下是一些值得关注的: