大多数React开发者并不会一次性破坏数据获取的过程。通常,这个问题会逐渐恶化,慢慢发展起来。

传统上,人们可能会在这里使用`useEffect`,在那里设置加载标志,并利用错误状态来处理数据获取相关逻辑。后续,新的数据获取操作会依赖于之前的结果,于是又需要另一个`useEffect`以及相应的加载和错误状态处理机制。

这种做法很可能会持续下去,直到你发现自己写的代码将来根本无法维护为止。

那些本应并行执行的请求却开始依次执行;组件为了满足另一个数据获取请求而被迫重新渲染,即使实际上并没有什么变化发生;页面上还会出现无意义的加载提示框,错误状态也散布在组件的各个部分中。

其实,这些都不是React本身的问题。它们都是你在编写React应用程序时应该了解的核心设计缺陷。

在这本手册中,我们将介绍一种React模式,这种模式能够在架构层面解决数据获取相关的问题,同时不会忽略实际的数据依赖关系,也不会引入任何复杂的机制。如果你觉得React中的数据获取操作过于繁琐,那么这种模式一定会让你感到更加清晰明了。

你将学习如何结合React的最新`use()` API与`Suspense`功能,来平滑地处理数据获取操作。在遇到错误时,你也会了解到如何利用`Error Boundary`来优雅地处理这些问题。

请仔细阅读这部分内容,并通过实际编码来加深对这种模式的理解。

这本手册还提供了视频教程,作为“15天学习React设计模式”项目的一部分。如果你感兴趣,可以去观看这些视频教程:

我们会使用大量的源代码来演示传统数据获取方式存在的问题,以及Suspense模式是如何改善这些问题的。我建议你在阅读的同时亲自尝试编写这些代码。不过如果你想提前查看源代码,也可以在tapaScript GitHub仓库中找到它们。

目录

  1. React中传统的数据获取方式

  2. 让我们使用传统的数据获取方式来构建仪表盘

  3. 什么是Suspense?

  4. React中的use() API是什么?

  5. 如何使用Suspense和use() API进行数据获取

  6. 让我们使用Suspense和use() API来构建仪表盘

  7. API服务

  8. 创建集中式的用户资源管理机制

  9. 创建独立的组件

  10. 创建备用用户界面

  11. 使用Suspense创建仪表盘组件

  12. 运行仪表盘应用程序

  13. 如何利用错误边界来处理错误场景?

  14. Suspense与错误边界

  15. 从“React设计模式15天学习计划”中汲取经验

  16. 在结束之前……

React中传统的数据获取方式

要理解为什么在React中数据获取会变得繁琐,我们首先需要了解React的内部工作原理。

React是分阶段工作的,并不会一次性完成所有操作。从宏观角度来看,React中的每一次更新都会经历三个不同的阶段:

  • 渲染阶段——React会确定UI应该呈现成什么样子

  • 提交阶段——React会将这些变化应用到DOM中

  • 效果阶段——React会与外部环境进行交互

这种分工是刻意设计的。正是这样的设计使得React具备可预测性、可中断性和高效性。

现在,让我们看看使用`useEffect`进行数据获取在整体流程中的位置:

The useEffect Phases

那么`useEffect`实际上是在什么时候执行的呢?它并不是在渲染阶段运行的,而是在React已经将UI提交到DOM之后才执行的。

因此,整个流程如下:

  1. React渲染组件(此时还没有获取数据)

  2. React将UI提交到DOM中

  3. `useEffect`开始执行

  4. 数据获取开始

  5. 当数据获取完成时,状态会更新

  6. React再次重新渲染组件

useEffect(() => {
  fetchData().then(setData);
}, []);

因此,数据获取操作只有在UI已经渲染完成之后才会开始。

传统方式存在的问题

举一个非常常见的例子:你首先获取用户的信息,然后使用该用户的ID去获取相关的其他数据。

传统的React数据获取解决方案可能如下所示:

useEffect(() => {
  fetchUser().then(setUser);
}, []);

useEffect(() => {
  return;
  fetchOrders(user.id).then(setOrders);
}, [user]);

实际发生的流程是:

  • 组件被渲染出来

  • React将UI提交到DOM中

  • 第一个`useEffect`生效,开始获取用户信息

  • React重新渲染组件

  • 第二个`useEffect`生效,开始获取用户的订单信息

即使网络速度很快,这些请求也必须依次执行,因为每次数据获取操作都是在前一次操作完成之后才会被触发。

这种数据获取模式被称为“渲染时才进行数据获取”。在这种情况下,数据获取的逻辑不再由数据之间的依赖关系来控制,而是由渲染的时机来决定的。但问题还不止这些——这种做法还会导致不必要的状态被创建和维护。

现在,让我们通过构建一些实际的应用程序来具体看看这两个问题是如何被解决的。

让我们使用传统的数据获取方式来构建一个仪表盘吧

让我们利用`useEffect`钩子,采用传统的数据获取方式来构建一个简单的仪表盘。这个仪表盘将包含四个主要部分:

  • 一个静态的标题。

  • 一个“个人资料”板块,会用用户的名字来欢迎他们。

  • 一个“订单”板块,会列出用户订购的商品。

  • 一个“分析数据”板块,会显示该用户的某些指标信息。

它的可视化效果大概是这样:

仪表盘

“个人资料”、“订单”和“分析数据”这几个板块应该能够显示用户的动态信息以及他们的订单详情和分析数据。因此,我们将模拟三次API调用,以便获取这些信息。

// 获取用户信息的API
export function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 1, name: "Tapas" });
    }, 1500");
  });
}

// 获取用户订单信息的API
export function fetchOrders(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
          `用户${userId}的订单A`,
          `用户${userId}的订单B`
      ]);
    }, 1500;
  });
}

// 获取用户分析数据的API
export function fetchAnalytics(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        revenue: "$12,000",
        growth: "18%",
        userId
      });
    }, 1500;
  });
}

如您在这段代码中看到的那样:

  • 每个API函数都会返回一个Promise对象。

  • 我们使用`setTimeout`故意设置了1.5秒的延迟,以此来模拟网络请求的实际过程。这个延迟时间结束后,Promise对象才会被解析。

  • 一旦Promise对象被解析,我们就能获取到相应的数据。

现在,让我们来创建“仪表板”组件:

import { useEffect, useState } from "react";
import { fetchAnalytics, fetchOrders, fetchUser } from "../api";

export default function Dashboard() const [user, setUser] = useState(null;
    const [orders, setOrders] = useState(nullconst [analytics, setAnalytics] = useState(null);

    // 第一步:获取用户信息
    useEffect(() => {
        fetchUser().then(setUser);
    }, []);

    // 第二步:获取订单信息(取决于用户是否存在)
    useEffect(() => {
        return;
        fetchOrders(user.id).then(setOrders);
    }, [user]);

    // 第三步:获取分析数据(取决于用户是否存在)
    useEffect(() => {
        return;
        fetchAnalytics(user.id).then(setAnalytics);
    }, [user]);

    // 确保用户信息、订单数据以及分析数据在渲染到JSX之前已经被获取到
    <p className="text-xl m-3">Loading dashboard...</span>p>
    }

    return (
        <div className="m-2">
            <header>
                <h1 className="text-5xl mb-12">📊 Dashboard</span>h1>
            </span>header>

            <h2 className="text-3xl">Welcome, {user.name}</span>h2>

            <h2 className="text-3xl mt-3">Orders</span>h2>
            <ul>
                {orders.map((o) => (
                    <li className="text-xl" key=</span>li>
                ))}
            </span>ul>

            <h2 className="text-3xl mt-3">Analytics</span>h2>
            <p className="text-xl">Revenue: {analytics.revenue}</span>p>
            <p className="text-xl">Growth: {analytics.growth}</span>p>
        </span>div>
    );
}

让我们来详细分析一下:

  • 首先会注意到的是,我们设置了三种状态来分别存储用户数据、订单信息以及分析数据。

  • 接着,我们使用了三个`useEffect`钩子来处理数据的获取以及状态的更新。

  • 之后,我们通过JSX将这些数据展示出来。

  • 我们采用了“在渲染时获取数据”的处理方式。

然而,在这个过程中,会有一套明确的逻辑来检查用户数据、订单数据或分析数据是否已经加载完成。如果这些数据尚未加载,我们就不会去处理JSX代码,而是会显示一条加载提示信息。

// 这段逻辑用于确保用户数据、订单数据和分析数据在被渲染到JSX之前已经加载完成
<pclassName"="text-xl m-3">正在加载数据...
</p>

这种处理方式可以有效地防止程序在运行时出现崩溃,但这种方式并不属于声明式编程。由于React本身就是一种声明式框架,因此如果我们能够以声明式的方式来解决这类问题,会更为理想。

在“声明式编程”中,程序员不需要指定具体的实现细节,只需要说明自己想要实现的目标,编程语言或框架会自动处理具体的实现方式。而当程序员去指定具体实现步骤时,这种编程方式就变成了命令式编程,而非声明式编程了。

React之所以属于声明式编程,是因为开发者无需自行指定如何更新浏览器DOM来显示UI的变化。只需使用JSX来描述这些变化,React会负责在后台完成相应的处理工作。

除了上述那种显式的命令式逻辑外,也可以通过设置加载状态来处理这类问题。例如可以为用户资料、订单信息和分析数据分别设置加载状态,从而根据这些状态条件性地显示相应的数据。不过这种方法需要额外的状态管理机制以及条件性的JSX渲染逻辑。

除此之外,还必须要考虑错误处理的问题!同样,也需要为错误处理设置相应的状态,并编写条件逻辑来决定何时显示错误信息。这样一来,状态管理的复杂度就会大大增加。

因此,使用`useEffect`钩子来进行数据获取并处理加载状态及错误情况,并不是一种非常高效的方法。我们需要寻找一种更好的方式来管理这些数据。

但在继续讨论之前,我想澄清一点:`useEffect`本身并不是一个糟糕的组件。它确实有其用途,但有时候我们并没有按照它的设计意图来使用它。如果你想学习如何有效地使用这个钩子,或者想要了解如何正确地调试它,你可以观看这个视频教程

什么是Suspense?

从根本上来说,React的功能并不是用于实现加载效果的,而是一种用于协调渲染过程的机制。通过 Suspense,一个组件可以告诉React:“我还没有准备好进行渲染”。当这种情况发生时,React会暂停对该部分代码的渲染,并显示一个替代界面,直到所需的数据准备就绪。

Suspense Mechanism

这与使用useEffect进行数据获取的方式有着本质上的区别。

在传统的Render-On-Load模式下,React必须先完成组件的渲染,才能开始获取数据。而useEffect会在渲染完成后才执行,因此数据获取总是作为渲染的后续步骤进行的,而不是渲染的前提条件。随着应用程序规模的增长,这种设计会导致出现“渲染→数据获取→再次渲染”的循环结构,以及分散在各个组件中的加载逻辑。

Suspense改变了这一模式。

它允许先进行数据获取,然后再进行渲染,即实现Render-as-you-Fetch机制。数据获取可以在React尝试更新UI之前就开始进行,而渲染过程则会一直等待数据准备就绪。UI不会自行判断何时显示加载状态,而是由React通过Suspense来统一协调这一过程。

在使用Suspense时,你需要将那些需要处理异步操作的组件包裹起来。

Suspense as Wrapper

当被包裹的组件正在处理异步请求时,Suspense可以暂停渲染过程,并显示一个替代界面。直到异步请求的结果确定后, Suspense才会用实际的数据结果替换这个替代界面。因此,完全不需要编写繁琐的逻辑代码或进行额外的状态管理。

React中的use() API是用来做什么的?

use()是React 19引入的一个API,它接受一个异步请求对象,并返回该请求的结果。如果请求结果尚未确定,React会暂停当前的渲染过程;如果请求失败,React会抛出错误。这两种情况都可以通过Suspense和Error Boundaries来优雅地处理。

import { use } from "react";

function fetchUser() return fetch("/api/user").then(res => res.json());
}

const userPromise = fetchUser();

export default Profile() const user = use(userPromise);
  return <h2>Welcome, {user.name}</h2>;
}

这里需要注意的关键点:

  • `use()`函数会在渲染过程中被调用

  • 如果某个Promise尚未解析完成,渲染过程将会暂停

  • 如果没有使用`useEffect`,就不会有加载状态

`use()`功能非常强大。它能够读取那些依赖于其他Promise的异步数据。

const userPromise = fetch("/api/user").then(r => r.json());

const ordersPromise = userPromise.then(user =>
  fetch(`/api/orders?userId=r => r.json())
);

function Orders() const orders = use(ordersPromise);
  return (
    <ul>
      {orders.map(o => <li key=</li>)}
    </ul>
  );
}

在这里:

  • 依赖项应该在数据中明确指定,而不是通过`useEffect`来处理

  • 渲染过程会自动进行协调(这是基于声明式的逻辑实现的)

核心的思维方式是:如果没有这些数据,当前这次的渲染是无法完成的,因此渲染过程会被暂停。

const data = use(promise);

我们需要使用`Suspense`来处理这种因Promise尚未解析而导致的延迟问题。在Promise解析完成之前,我们可以使用替代界面来显示内容;而一旦Promise解析完毕,渲染过程就会继续进行。

如何使用`Suspense`以及`use()` API来进行数据获取

正是`use()` API让`Suspense`在数据获取场景中真正发挥了作用。在`use()`出现之前,虽然`Suspense`可以暂停渲染过程,但React并没有一种简洁的方法来在渲染过程中处理异步数据,因此大多数情况下人们都需要借助自定义的抽象层或第三方库来解决这个问题。而`use()`的出现改变了这一现状,它让React组件能够在渲染过程中直接获取异步数据。

当一个组件使用`use(promise)`来获取数据时,React会将该Promise视为渲染所需的依赖项。如果该Promise尚未解析完成,React会在最近的`Suspense`作用域处暂停渲染;而一旦Promise解析完毕,React会自动重新开始渲染过程,无需人工更新状态、执行副作用代码或编写条件逻辑。

import { Suspense, use } from "react";

const userPromise = fetch("/api/user").then(res => res.json());

function Profile() {
  const user = use(userPromise);
  return <h2>Welcome, {user.name}</h2>>;
}

export default function App() {
  return (
    <Suspense fallback=p>Loading profile...</p>>}>
      <Profile/>
    </Suspense>>
  );
}

这里发生了什么:

  • `Profile`函数尝试获取`userPromise`的值。

  • 如果这个承诺没有得到解决,React会暂停渲染过程。

  • 此时,React会显示`Suspense`提供的备用内容。

  • 一旦该承诺的值确定,React会自动重新开始渲染。

在这里,不会出现任何加载提示、动画效果,也不需要手动重新渲染页面。

现在,让我们通过重新构建同一个仪表板应用来实际体验这些功能。

让我们使用Suspense和`use()` API来构建仪表板吧

既然我们已经对Suspense和`use()`有了更深入的了解,那么我们就用它们来重新编写同一个仪表板应用吧。

项目设置

首先,你需要使用Vite创建一个React项目的框架结构。你可以使用以下命令来创建一个配备现代工具的Vite基React项目:

npx degit atapas/code-in-react-19#main suspense-patterns

这样就会创建一个配置了TailwindCSS的React 19项目。

接下来,使用`npm install`命令来安装所需的依赖项。系统会自动为你生成`node_modules`文件夹。此时,目录结构应该如下所示:

React 19项目目录结构

API服务

现在,在src/目录下创建一个名为api/的新文件夹。接着在src/app/目录下创建一个index.js文件,并在其中写入以下代码:

export function fetchUser() {  
  return new Promise((resolve) => {  
    setTimeout(() => {  
      resolve({ id: 1, name: "Tapas" });  
    }, 1500;  
  });  

  export function fetchOrders(userId) {  
    return new Promise((resolve, reject) => {  
      setTimeout(() => {  
        resolve([
          `用户${userId}的订单A`,  
          `用户${userId}的订单B`  
        ];  
      }, 1500;  
    });  

  export function fetchAnalytics(userId) {  
    return new Promise((resolve) => {  
      setTimeout(() => {  
        resolve({  
          revenue: "$12,000",  
          growth: "18%",  
          userId  
        });  
      }, 1500;  
    };  
  }  
}

这些就是我们之前在使用useEffect构建仪表板时所使用的那些API。

  • fetchUser:用于获取用户的个人资料信息。

  • fetchOrders:用于获取用户下达的订单信息。

  • fetchAnalytics:用于获取用户的分析数据。

创建集中式的用户资源文件

现在,让我们创建一个集中的JavaScript工具文件,在其中可以通过调用相应的`fetch`方法来生成各种Promise对象。将所有的`fetch` API及其对应的Promise对象集中处理在一个地方,而不是让它们分散在各个地方,这是一种良好的实践。这个工具文件还可以导出这些Promise对象,这样我们就可以在组件中使用它们了。

在`src/`目录下创建一个`resources/`文件夹,在`src/resources/`文件夹下创建一个名为`userResource.js`的文件,并在其中编写以下代码:

import { fetchAnalytics, fetchOrders, fetchUser } from "../api";

let userPromise;
let ordersPromise;
let analyticsPromise;

export createUserResources() {
  userPromise = fetchUser();

  ordersPromise = userPromise.then(user =>
    fetchOrders(user.id)
  );

  analyticsPromise = userPromise.then(user =>
    fetchAnalytics(user.id)
  );
}

export getUserResources() {
  return {
    userPromise,
    ordersPromise,
    analyticsPromise
  };
}

在这里,我们导出了两个函数:

  1. `createUserResources()`函数用于生成所有的Promise对象,并将它们准备好供后续使用。

  2. `getUserResources()`函数则返回所有这些Promise对象,我们可以稍后使用它们。

那么问题来了:我们什么时候应该生成这些Promise对象呢?也就是说,我们应该在什么地方调用`createUserResources()`函数呢?我们应该在应用程序启动时生成这些Promise对象,而`main.jsx`文件正是执行这一操作的理想位置。

打开`main.jsx`文件,导入`{createUserResources}`,然后立即调用它。

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import  "./index.css";

import { createUserResources } from "./resources/userResource.js";

createUserResources();

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <App />
    </ReactStrictMode>,
);

太好了!我们的数据获取API以及相关的Promise对象已经准备就绪了。接下来,我们就来创建那些会使用这些Promise对象的组件吧。

创建单独的组件

我们将创建三个组件来构建仪表盘:个人资料组件、订单组件和分析组件。

组件层次结构

让我们先从个人资料组件开始吧。在src/目录下创建一个components/文件夹,然后在这个文件夹中创建一个Profile.jsx文件,并编写如下代码:

import { use } from "react";
import { getUserResources } from "../resources/userResource";

export default function Profile() {
    const { userPromise } = getUserResources();
    const user = use(userPromise);
    return <h2 className="text-3xl">Welcome, {user.name}</h2>;
}

让我们来详细分析一下这段代码:

  • 我们从React中导入了use()函数,因为我们需要使用这个函数来处理Promise对象,并获取用户的名字以便进行渲染。

  • 接下来,我们需要获取用户的Promise对象。为此,我们使用了getUserResources()函数,因此将其导入到了代码中。

  • 在个人资料组件内部,我们从getUserResources()函数返回的结果中提取出了userPromise对象。

  • 之后,我们将这个Promise对象传递给了use()函数。由于use()函数会接收一个Promise对象,并在它被解析后返回结果,在解析之前,它会直接返回传入的Promise对象本身。

  • 最后,我们利用被解析后的用户信息提取出了名字属性,并将其进行了渲染。

很简单吧?那么我们就快速创建订单组件和分析组件吧。

订单组件的代码如下:

import { use } from "react";
import { getUserResources } from "../resources/userResource";

export default function Orders() {
   const { ordersPromise } = getUserResources();
  const orders = use(ordersPromise);

  return (
    <>
      h2 className="text-3xl mt-2">>订单</h2>
      ul>>
        {orders.map((o) => (
          <li className="text-xl" key=</li>>
        ))}
      ul>>
    import { use } from "react";
import { getUserResources } from "../resources/userResource";

export default Analytics() {
    const { analyticsPromise } = getUserResources();
    const analytics = use(analyticsPromise);

    return (
        <>
            <h2 className="text-3xl mt-2">Analytics</h2>
            <p className="text-xl">收入:{analytics.revenue}</p>
            <p className="text-xl">增长率:{analytics.growth}</p>
        </>
    );
}

这三个组件都已经准备好了。在继续下一步之前,让我们再回顾一下关于`Suspense`的知识。

`Suspense`用于包裹那些处理异步操作的组件。在异步操作的结果尚未确定之前,`Suspense`会暂停组件的渲染过程,并显示一个备用界面;一旦异步操作的结果确定下来,备用界面就会被被包裹的组件所替代。

现在,我们已经有了理想的做法:用`…</Suspense>`这个结构来包裹``、``和``这三个组件,这样就可以分别处理它们对应的异步操作及其结果了。

让我们就这样去做吧……不过,我们是不是遗漏了什么?没错,我们确实遗漏了:备用界面。那么,我们就来创建它吧。

创建备用界面

现在我们将创建三个不同的备用界面组件。在`src/components/`目录下创建一个名为`Skeletons.jsx`的文件,并在其中编写以下代码:

export const ProfileSkeleton = () => <p className="text-3xl m-2">正在加载用户信息……p>;
export const OrdersSkeleton = () => <p className="text-3xl m-2">正在加载订单信息……p>;
export const AnalyticsSkeleton = () => <p className="text-3xl m-2">正在加载分析数据……p>;

我们现在为每个组件都准备了一个备用的基础界面框架。这些组件非常简单,它们的作用仅仅是显示加载提示信息。

使用 Suspense 功能创建仪表盘组件

现在我们已经具备了让仪表盘正常运行的所有条件。在 `src/` 目录下创建一个名为 `suspense/` 的文件夹,然后在该文件夹中创建一个名为 `Dashboard.jsx` 的文件,并写入以下代码:

import { Suspense } from "react";

import Analytics from "../components/Analytics";
import Orders from "../components/Orders";
import Profile from "../components/Profile";

import {
    AnalyticsSkeleton,
    OrdersSkeleton,
    ProfileSkeleton,
} from "../components/Skeletons";

export default Dashboard() {
    return (
        <div className="m-2">
            <header>
                <h1 className="text-5xl mb-12">📊 仪表盘</h1>
            </header>

            <Suspense fallback={<ProfileSkeleton />}>
               <Profile />
            </Suspense>
            <Suspense fallback=<OrdersSkeleton />}>
               <Orders />
            </Suspense>
            <Suspense fallback=<AnalyticsSkeleton />}>
               <Analytics />
            </Suspense>
        </div>
    );
}

首先,让我来解释一下这段代码:

  • 我们从React中导入了Suspense组件,以及所有的组件和备用UI组件。

  • 然后,我们为每个组件渲染了一个静态标题栏,以及三个用于处理数据加载延迟的 Suspense 结构。我们分别用Suspense包装了Profile、Orders和Analytics这三个组件。为了应对这些组件在数据获取过程中可能出现的延迟问题,我们为Suspense指定了相应的备用组件作为替代选项。

这种写法到底有多简洁呢?如果你向上滚动页面,重新查看我们之前使用`useEffect`实现的仪表板代码,再与我们现在用Suspense实现的版本进行对比,就会发现其中的优势非常明显。

  • 这种写法更加声明式,结构更加清晰。

  • 代码量更少,因此出现错误的概率也更低。

  • 不需要进行任何效果管理或同步操作。

  • 也没有条件性的JSX代码。

这确实是一个巨大的进步啊 🏆。

运行仪表板应用

要运行这个仪表板应用,只需在`App.jsx`文件中导入Dashboard组件,然后像下面这样使用它即可:

import Dashboard from "./suspense/Dashboard";
function App() return (
        <div className="flex items-center justify-center gap-12">
            <Dashboard />
        </div>
    );
}

export default App;

接下来,打开终端,使用`npm run dev`命令运行应用程序。你会看到同样的仪表板界面,但它已经得到了极大的改进:

新的仪表板

  • 各个部分的数据都是独立加载的。

  • 当数据获取请求处于等待状态时,每个部分都会显示相应的加载指示器。

  • 这种设计不会导致整个用户界面陷入等待状态。

Suspense与`use()`结合使用确实非常强大。现在你已经掌握了这一非常实用的开发模式。

如何利用错误边界处理错误场景

如果不对错误处理方式进行讨论,这份关于数据获取的指南就不算完整。到目前为止,我们只讨论了正常情况下的开发流程。但如果其中某个数据请求失败了,我们应该如何应对呢?

为了深入理解这一点,我们先放弃其中一个承诺吧——比如说获取订单的承诺。打开位于 `src/api/` 文件夹下的 `index.js` 文件,将其中的 `fetchOrder()` 函数替换为以下更新后的代码:

export function fetchOrders(userId) {  
  return new Promise((resolve, reject) => {  
    setTimeout(() => {  
      // 模拟失败情况  
      Math.random() < 0.5) {  
        reject(new Error(“无法获取订单信息”));  
      } `用户 ${userId} 的订单 A`,  
          `用户 ${userId} 的订单 B`  
        });  
      }  
    }, 1500;  
  });  
}

这里所做的改动包括:

  • 我们通过拒绝某个承诺来模拟失败情况。

  • 这个承诺会以随机的方式被拒绝,从而引发错误并显示相应的错误信息。

如果此时你多次刷新页面,就会看到空白且出现错误的界面,同时浏览器控制台也会显示出错误信息。这种情况显然很不理想,因为它会严重影响应用程序的使用体验。

一种更好的处理方式是在界面上显示错误信息,并提供重新尝试的功能,让用户能够从错误中恢复过来。

这时,《Error Boundary》就派上用场了。

Error Boundary

在 React 中引入 Error Boundary 的原因很简单:错误是不可避免的,我们必须以恰当的方式处理它们。可能遇到的错误情况包括:

  • 网络请求失败

  • 数据格式不正确

  • 某些假设条件不成立

如果没有 Error Boundary,即使是一个小小的渲染错误也可能会导致整个 React 应用程序崩溃。Error Boundary 为 React 提供了一种结构化的方式来处理这些错误。

从技术上讲,Error Boundary 是一种能够捕获在渲染过程中出现的错误的组件。当错误发生时,React 会停止渲染相关的子树,转而显示替代的界面。

现在我们就来创建一个 Error Boundary 组件吧。在 `src/components` 文件夹下创建一个名为 `ErrorBoundary.jsx` 的文件,并填写以下代码:

“`javascript

“`

现在,让我们来了解一下上面代码中发生了什么:

  • 这是一个继承自React.Component的类组件。这是因为错误处理机制必须使用类组件的生命周期方法,而这些方法在函数组件中是不可用的。

  • 这个组件会记录是否发生了错误。state = { error: null }表示一切都在正常渲染。当出现错误时,这个状态变量就会存储错误对象。

  • static getDerivedStateFromError()是一种特殊的生命周期方法。当子组件在渲染过程中抛出错误时,React会自动调用这个方法。

  • handleRetry()方法会将错误状态重置为null,并调用createUserResources()来重新初始化异步资源。

  • render()方法中,如果存在错误,就会渲染替代界面、显示错误信息,并提供通过“重试”按钮来重新尝试的功能。如果没有错误,就会正常渲染children内容。当一切运行顺利时,错误处理机制就会变得不可见。替代界面也可以是一个外部组件,我们可以通过属性将其传递给错误处理组件。

如果你有兴趣深入了解Error Boundary模式,并想了解它的各种使用场景,这里有一个专门的视频供你参考。

悬停效果与错误处理机制

接下来,我们将使用Error Boundary来包裹每一个悬停效果相关的组件,这样一旦其中某个组件出现错误,就可以得到及时的处理。请打开Dashboard.jsx文件,按照下面的方式用组件包裹每个悬停效果相关的内容:

import { Suspense } from "react";
import Analytics from "../components/Analytics";
import ErrorBoundary from "../components/Error Boundary";
import Orders from "../components/Orders";
import Profile from "../components/Profile";
import {
    AnalyticsSkeleton,
    OrdersSkeleton,
    ProfileSkeleton,
} from "../components/Skeletons";

export default function Dashboard() {
    return (
        <div className="m-2">
            <header>
                <h1 className="text-5xl mb-12">📊 Dashboard</h1>
            </header>

            <ErrorBoundary>
                <Suspense fallback={<ProfileSkeleton />}>
                    <Profile />
                </Suspense>
            </ErrorBoundary>

            <ErrorBoundary>
                <Suspense fallback=OrdersSkeleton />}>
                    <Orders />
                </Suspense>
            </ErrorBoundary>

            <ErrorBoundary>
                <Suspense fallback=AnalyticsSkeleton />}>
                    <Analytics />
                </Suspense>
            </ErrorBoundary>
        </div>
    );
}

就是这样。现在,通过浏览器访问控制面板吧。每当订单处理被拒绝时,我们就会从错误处理机制中看到相应的错误提示界面。需要注意的是,其余的界面功能并未受到影响,依然能够正常显示。由于我们提供了重试按钮,因此这部分出错的界面也是可以恢复正常的,这样的设计确实为用户提供了很好的使用体验。

带有暂停功能的错误处理机制

正是通过这种错误处理机制、use() API以及相关的设计模式,我们才能编写出可扩展的React代码,而这些代码在未来也更容易维护。希望这些内容对您有所帮助。本手册中使用的所有源代码都可以在tapaScript GitHub仓库中找到。

从“15天React设计模式学习计划”中获益

我有个好消息要告诉您:在我进行了为期40天的JavaScript学习活动之后,我现在又完成了一个全新的学习项目,那就是“15天React设计模式学习计划”(还包括额外的扩展内容)。

如果您觉得这本手册对您有帮助,那么您肯定也会喜欢这个系列视频。这个系列涵盖了15种最重要的React设计模式,快来观看吧!订阅后还可以免费获取:

15天React设计模式学习计划

在结束之前……

就这些了!希望这些内容对您有所帮助。

让我们保持联系吧:

<期待在下一篇文章中与大家再见。在此之前,请照顾好自己,继续努力学习吧。>

Comments are closed.