[译] 实战 React 18 中的 Suspense

2023-12-06 16:24:01 浏览数 (2)

> 原文:https://dev.to/darkmavis1980/a-practical-example-of-suspense-in-react-18-3lln

React 18 带来了很多变化,它不会破坏你已经编写过的代码,并且有很多改进和一些新概念。

它也让很多开发人员,包括我,意识到我们错误地使用了useEffect hook。但话说回来,我们被其名称所误导了,因为实际上useEffect并不应该被用于副作用。

在 React 18 中,虽然仍然可以使用useEffect来完成一些事情,如使用 API 接口读取的数据填充状态,但实际上不应该将其用于此类目的。如果你在应用程序中启用StrictMode,在开发模式下,你将发现使用useEffect会被调用两次,因为现在React会mount 组件、卸载它,然后再次 mount 它,以检查代码是否运行正常。

Suspense 来了

我们应该用来取而代之的,是新的Suspense组件(虽然它已经存在于 React 17 中,但现在是推荐的方法),此组件将会按照以下方式工作:

代码语言:javascript复制
<Suspense fallback={<p>Loading...</p>}>
  <MyComponent />
</Suspense>

上面的代码将会包裹一个组件,这个组件从某些数据源中加载数据,并在完成数据获取之前显示fallback。

Suspense 是什么

简而言之,可能和你想的不同,Suspense 并不是一个新的用于获取数据的接口,因为该工作仍然由诸如“fetch”或“axios”等库委派执行,而它实际上允许你将这些库与 React 集成,并且它的真正工作只是“在加载时显示这段代码,而在完成后显示那段代码”,仅此而已。

Suspense 如何工作

首先,你需要了解 Promise 的工作原理以及它的状态。无论使用传统方式new Promise()还是新的async/await语法来使用promise,在任何情况下,promise始终具有以下这三种状态:

  • pending -> 它仍在处理请求
  • resolved -> 请求已返回某些数据,我们获得了200 OK状态
  • rejected -> 出现了错误,获得了一个错误

Suspense使用的逻辑与ErrorBoundary完全相反,因此如果代码引发异常(因为它仍处于加载状态或者由于加载失败),则显示fallback;如果成功解析,则显示子组件。

举个例子

来看一个简单的例子,我们只需创建一个组件来获取API中的某些数据,并且希望在准备好后渲染该组件。

注意 为了简化,这里不会提到如何使用“startTransition”,添加错误边界,甚至不会涉及各种策略之间的区别,例如“fetch-on-render”、“fetch-then-render”等等...

包装 fetch 逻辑

如上所述,当我们的组件正在加载数据或失败时,需要抛出异常,但是一旦成功解决了Promise,就可以简单地返回响应。

为此,我们需要使用以下函数包装我们的请求:

代码语言:javascript复制
// wrapPromise.js
/**
 * 将promise包装,以便可以与React Suspense一起使用
 * @param {Promise} 要处理的promise
 * @returns {Object} 与Suspense兼容的响应对象
 */
function wrapPromise(promise) {
  let status = 'pending';
  let response;

  const suspender = promise.then(
    res => {
      status = 'success';
      response = res;
    },
    err => {
      status = 'error';
      response = err;
    },
  );

  const handler = {
    pending: () => {
      throw suspender;
    },
    error: () => {
      throw response;
    },
    default: () => response,
  };

  const read = () => {
    const result = handler[status] ? handler[status]() : handler.default();
    return result;
  };

  return { read };
}

export default wrapPromise;

因此,上面的代码将检查我们Promise的状态,然后返回一个名为“read”的函数,稍后我们将在组件中调用它。

现在,我们需要使用它包装接口请求库(例子中是axios),创建一个非常简单的函数:

代码语言:javascript复制
//fetchData.js
import axios from 'axios';
import wrapPromise from './wrapPromise';

/**
* 用wrapPromise函数包装Axios请求
* @param {string} 要获取的URL
* @returns {Promise} 包装的promise
*/
function fetchData(url) {
     const promise = axios.get(url).then(({data}) => data);

     return wrapPromise(promise);
}

export default fetchData;

这只是以接口请求库表现的一种抽象,我想强调这只是一种非常简单的实现,您可以将上面的所有代码扩展到任何需要做的工作中。在这里我使用了axios,但你可以根据自己的需要使用任何东西。

在组件中读取数据

当获取方面的所有内容都准备好后,我们来在组件中使用它。假设有一个简单的组件,只需从某个接口读取名称列表并打印。不同于习惯中在组件中通过useEffect钩子调用 fetch 的做法,这一次我们要直接在组件开始时(放在任何 hooks 之外),使用我们在包装器中导出的read方法来调用请求,因此我们的Names组件大概是这个样子的:

代码语言:javascript复制
// names.jsx
import React from 'react';
import fetchData from '../../api/fetchData.js';

const resource = fetchData('/sample.json');
const Names = () => {
  const namesList = resource.read();

  // rest of the code
}

这里所做的是,当调用组件时,read()函数将开始抛出异常,直到完全解析完成;其后,会继续执行其余代码,在此例中也就是继续 render。所以该组件的全部代码如下:

代码语言:javascript复制
// names.jsx
import React from 'react';
import fetchData from '../../api/fetchData.js';

const resource = fetchData('/sample.json');

const Names = () => {
  const namesList = resource.read();

  return (
    <div>
      <h2>List of names</h2>
      <ul>
        {namesList.map(item => (
          <li key={item.id}>
            {item.name}
          </li>))}
      </ul>
    </div>
  );
};

export default Names;

父组件

现在 Suspense 将要发挥作用了,首先需要在父组件中导入它:

代码语言:javascript复制
// parent.jsx
import React, { Suspense } from 'react';
import Names from './names';

const Home = () => (
  <div>
    <Suspense fallback={<p>Loading...</p>}>
      <Names />
    </Suspense>
  </div>
);

export default Home;

到底肿么了? 我们将Suspense作为React组件导入,然后使用它来包装获取数据的组件,在这些数据被 resolve 之前,它将只会渲染“fallback”组件,因此只是<p>Loading...</p>或其他什么你需要的自定义组件。

结论

长时间使用useEffect以实现相同的结果后,当我第一次看到 Suspanse 这种用法时,我对这种新方法有些怀疑。包装获取库的整个过程有点让人生疑。但是现在,我可以看到它的好处,它非常容易处理加载状态,它抽象掉了一些代码,使其易于重用,并通过消除(好吧,至少在大多数情况下)组件本身的“useEffect”钩子简化了组件的代码,这在以前可是个让人头疼的事情。

0 人点赞