React中加载、错误和空状态的UI最佳实践

401 阅读9分钟

构建深思熟虑的、有创意的UI是很困难的。即使是优秀的UX/UI设计,也无法讲述一个网络应用的全部故事。

因为它们只是内在动态事物的静态表现,所以要由开发者来使设计变得生动,当然,这意味着要考虑到所有可能的状态。

在这篇文章中,我们将介绍在客户端渲染的React应用中处理加载、错误和空状态时的最佳实践。

React组件

想象一下,我们想用React和Chakra UI在我们正在构建的单页应用程序(SPA)的背景下重新创建维基百科的猫品种列表中的表格。

下面的图片就是我们要实现的设计。够简单了吧?

Cats Table

首先,让我们把我们的猫品种数据提取到一个单独的文件,data.json

[  {    "name": "Abyssinian",    "origin": "Unspecified, but somewhere in Afro-Asia likely Ethiopia",    "type": "Natural",    "bodyType": "Semi-foreign",    "coat": "Short",    "pattern": "Agouti",    "imgUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9b/Gustav_chocolate.jpg/200px-Gustav_chocolate.jpg"  },  {    "name": "Aegean",    "origin": "Greece",    "type": "Natural",    "bodyType": "Moderate",    "coat": "Semi-long",    "pattern": "Multi-color",    "imgUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/51/Aegean_cat.jpg/200px-Aegean_cat.jpg"  },  ...]

我们现在可以创建一个依赖这些数据的TableComponent

import { Table, Thead, Tbody, Tr, Th, Td } from "@chakra-ui/react";
import data from "../public/data.json";

const TableComponent = () => {
  return (
      <Table colorScheme="blue" overflow="none">
        <Thead>
          <Tr>
            <Th></Th>
            <Th>Name</Th>
            <Th>Origin</Th>
            <Th>Type</Th>
            <Th>Body type</Th>
            <Th>Coat</Th>
            <Th>Pattern</Th>
          </Tr>
        </Thead>
        <Tbody>
          {data.map((cat) => (
            <Tr key={cat.name}>
              <Td p="2">
                <Box
                  bgImage={cat.imgUrl}
                  w="150px"
                  h="150px"
                  backgroundSize="cover"
                />
              </Td>
              <Td>{cat.name}</Td>
              <Td>{cat.origin}</Td>
              <Td>{cat.type}</Td>
              <Td>{cat.bodyType}</Td>
              <Td>{cat.coat}</Td>
              <Td>{cat.pattern}</Td>
            </Tr>
          ))}
        </Tbody>
      </Table>
  );
};

完成了!它看起来和预期的完全一样。

但是这个实现也是非常幼稚的!我们的猫表的数据是简单的硬编码。在一个真实世界的React应用中,它很可能来自服务器。这又意味着,当用户打开页面时,它不会立即可用。我们必须要等待它。

加载状态

现在我们来到了第一个没有明确包含在上面图片中的状态,即加载状态。当我们请求数据时,我们需要向我们的用户表明,在我们等待服务器响应时,有东西正在加载。否则,他们将只看到一个空白的屏幕。

因此,让我们在我们的TableComponent ,添加一个Spinner

import React, { useState } from "react";
import {
  ...
  Spinner
} from "@chakra-ui/react";
import useCats from "./useCats";

const TableComponent = () => {
  // Custom data-fetching hook to handle the server request
  const { data, isLoading } = useCats();

  if (isLoading) {
    return <Spinner />;
  }

  return (
    <Table>
      ...
    </Table>
  );
};

这看起来已经更现实了。但这里有一个想法:我们是否需要在每次获取表格数据时都显示一个旋转器?

例如,如果我们想在不同的页面之间进行导航呢?如果每当用户进入一个新的页面时,表格就会消失,而旋转器就会出现,这将是一种破坏。这同样适用于我们实现搜索。

或者,如果我们已经在我们的应用程序中缓存了表的数据,但我们仍然想在后台重新获取它,以确保它不会变质,那怎么办?再一次,当我们重新获取的时候,对用户来说,能够看到已经可用的数据而不是一个旋转器会好很多。

为了使我们的用户界面真正感觉良好,我们需要向用户指出后台正在发生的事情。看起来我们需要第二个加载状态,一个允许我们同时显示我们的TableComponent

让我们添加它吧!

import React, { useState } from "react";
import {
  ...
  Spinner,
  Progress,
} from "@chakra-ui/react";
import useCats from "./useCats";

const TableComponent = () => {
  const { data, isLoading } = useCats();

  if (isLoading && !data) {
    return <Spinner />;
  }

  return (
    <>
      {isLoading && (
        <Progress />
      )}
      <Table>
        ...
      </Table>
    </>
  );
};

这很好!我们现在只在没有数据要显示给用户时才显示我们的Spinner 。否则,我们就在表格的顶部显示更微妙的Progress

但是,如果根本就没有数据呢?

React中的空状态

因为我们的数据来自服务器,我们不能确定是否有数据。我们可能会收到一个空列表。如果这种情况发生在我们目前的实现中,用户将只看到Table headers,这将是非常混乱的。

我们应该给我们的组件添加一个带有适当信息的空状态,以考虑到这种情况。

import React, { useState } from "react";
import {
  ...
  Spinner,
  Progress,
  Text
} from "@chakra-ui/react";
import { SearchIcon } from "@chakra-ui/icons";
import useCats from "./useCats";

const EmptyState = () => {
  return (
    <Box>
      <SearchIcon />
      <Text>
        No cats found!
      </Text>
    </Box>
  );
};

const TableComponent = () => {
  const { data, isLoading } = useCats();

  if (isLoading && !data) {
    return <Spinner />;
  }

  if (data?.length === 0) {
    return <EmptyState />;
  }

  return (
    <>
      {isLoading && (
        <Progress />
      )}
        <Table>
          ...
        </Table>
    </>
  );
};

完美!我们就快完成了。我们几乎已经完成了。只有最后一件事需要考虑。

React中的错误状态

因为我们的数据依赖于服务器的响应,所以考虑我们的请求失败的情况是很重要的。如果是这种情况,我们需要向用户显示一个适当的错误信息。

让我们来做吧。

import React, { useState } from "react";
import {
  ...
  Alert,
  AlertIcon,
  AlertTitle,
  AlertDescription
} from "@chakra-ui/react";
import useCats from "./useCats";
...
const ErrorState = () => {
  return (
    <Alert status="error">
      <AlertIcon />
      <AlertTitle mr={2}>An error occured!</AlertTitle>
      <AlertDescription>Please contact us for assistance.</AlertDescription>
    </Alert>
  );
};

const TableComponent = () => {
  const { data, isLoading, isError } = useCats();

  if (isError) {
    return <ErrorState />;
  }

  if (isLoading && !data) {
    return <Spinner />;
  }

  if (data?.length === 0) {
    return <EmptyState />;
  }

  return (
    <>
      {isLoading && (
        <Progress size="xs" isIndeterminate w="100%" position="fixed" top="0" />
      )}
      <Table colorScheme="blue" overflow="none">
        ...
      </Table>
    </>
  );
};

我们的组件现在工作得很完美。我们已经真正地考虑到了所有可能的情况。对这些代码感到好奇吗?自己在这里玩一玩吧。

请记住,以上是一个基于单一表格组件的简化例子。在一个真实世界的应用程序中,可能有几十个页面,有许多表格和组件,它们都需要有自己的加载、错误和空状态。

所以,你可能会想:有没有办法大规模地实现我们的方法?

规模化获取数据的UI

为了给我们的客户端渲染的React应用实现一个可扩展的数据获取解决方案,我们首先需要有合适的工具供我们使用。这里有一些有用的提示。

首先,使用一个成熟的、经过战斗考验的数据获取库来处理数据同步和缓存。从头开始创建这样的东西,既困难又没有必要。

在这方面有很多好的选择。我个人的偏好是React Query。一旦我们开始使用数据获取库,为我们可能需要的每个请求实现一个自定义的React钩子就变得微不足道了,类似于我们的例子useCats

如上面的例子所示,这些钩子包含了我们实现每个组件的数据获取状态所需的所有信息。

然后,利用组件库的优势,或者至少在构建加载、错误和空状态组件时考虑到可重复使用性。在我们的例子中,我们使用Chakra UI作为基本的构建模块。但我们也将我们的ErrorStateEmptyState 组件从主表中分离出来,确保我们可以在应用程序的其他地方再次使用它们。

最后,整个应用的UX/UI一致性对用户来说真的很重要。为了进一步提高它,请确保将处理数据获取状态的方式标准化。这可以通过进一步抽象我们上面使用的条件渲染逻辑来实现。

如果由于某些原因,抽象化不是一个好的选择,确保至少通过惯例和/或林特规则强烈鼓励整个代码库的标准化。

用于数据获取的React Suspense

在我们结束这个概述之前,有必要提到一个重要的React更新正在进行中--用于数据获取的React Suspense。这个新功能将允许它声明性地等待任何异步的东西,包括数据。在许多方面,这将是我们在React中对数据思考的一个范式转变,因为我们从 "读取时获取 "转换到 "获取时渲染 "的方法(关于这一转变的更多信息可以在这里找到)。

让我们用Suspense重新实现我们的组件,看看它是如何工作的。首先,我们需要为我们的数据创建一个假的API。

import data from "./data.json";

export function fetchData() {
  let catsPromise = fetchCats();
  return {
    cats: wrapPromise(catsPromise)
  };
}

function wrapPromise(promise) {
  let status = "pending";
  let result;
  let suspender = promise.then(
    (r) => {
      status = "success";
      result = r;
    },
    (e) => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}

function fetchCats() {
  console.log("fetch cats...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("fetched cats");
      resolve(data);
      // reject();
    }, 1100);
  });
}

注意:这只是一个实现的例子,并不是可用于生产的代码!

用React Suspense加载状态

有了API,我们可以将我们的加载状态添加到Suspense fallback。

import React, { Suspense } from "react";
import { ChakraProvider } from "@chakra-ui/react";
import * as ReactDOM from "react-dom";
import Table from "./Table";
import LoadingState from "./LoadingState";

const rootElement = document.getElementById("root");
ReactDOM.createRoot(rootElement).render(
  <ChakraProvider>
    <Suspense fallback={<LoadingState />}>
      <Table />
    </Suspense>
  </ChakraProvider>
);

并相应地改变我们的组件。

import React from "react";
import { Table, ... } from "@chakra-ui/react";
import { SearchIcon } from "@chakra-ui/icons";
import { fetchData } from "./fakeApi";
import EmptyState from "./EmptyState"

const data = fetchData();

const TableComponent = () => {
  const cats = data.cats.read();

  if (cats?.length === 0) {
    return <EmptyState />;
  }

  return (
      <Table colorScheme="blue" overflow="none">
        ...
      </Table>
  );
};
export default TableComponent;

很好,我们现在正在使用Suspense 来获取数据!另外,正如我们所看到的,空状态的实现仍然和以前一样。

但是我们的错误处理发生了什么?

使用React Suspense的错误状态

使用Suspense 来获取数据的另一个有趣的变化是,我们可以用处理渲染错误的方式来处理获取的错误。为此,我们将需要使用--你猜对了--ErrorBoundary

让我们把它添加到我们的应用程序中。

import React, { Suspense } from "react";
import { ChakraProvider } from "@chakra-ui/react";
import * as ReactDOM from "react-dom";
import Table from "./Table";
import LoadingState from "./LoadingState";
import ErrorState from "./ErrorState";
import ErrorBoundary from "./ErrorBoundary";

const rootElement = document.getElementById("root");
ReactDOM.createRoot(rootElement).render(
  <ChakraProvider>
    <ErrorBoundary fallback={<ErrorState />}>
      <Suspense fallback={<LoadingState />}>
        <Table />
      </Suspense>
    </ErrorBoundary>
  </ChakraProvider>
);

完美,现在我们能够像以前一样显示我们的错误组件了。请看这个代码沙盒的工作实例。

使用这些预期的新功能有几个好处。在它们的帮助下,我们可以通过放置SuspenseErrorBoundary 组件来细化控制我们应用程序中的错误和加载状态。

同时,数据获取和渲染逻辑保持解耦(获取在渲染之前启动)。通过Suspense ,还可以避免经常困扰更多传统方法的竞赛条件

虽然React Suspense本身不是一个数据获取库,但正在采取措施将其整合到现有的解决方案中。

这些新的React更新是非常令人兴奋的,也是非常有趣的玩法!但重要的是要记住,这一切仍然是实验性的,可能会有变化。为了获得最新的信息,你可以关注React 18工作组的讨论。

如果你觉得这篇文章有用,请在Twitter上关注我,并访问我的博客了解更多技术内容。

编码愉快!✨

The postUI best practices for loading, error, and empty states in Reactappeared first onLogRocket Blog.