构建深思熟虑的、有创意的UI是很困难的。即使是优秀的UX/UI设计,也无法讲述一个网络应用的全部故事。
因为它们只是内在动态事物的静态表现,所以要由开发者来使设计变得生动,当然,这意味着要考虑到所有可能的状态。
在这篇文章中,我们将介绍在客户端渲染的React应用中处理加载、错误和空状态时的最佳实践。
React组件
想象一下,我们想用React和Chakra UI在我们正在构建的单页应用程序(SPA)的背景下重新创建维基百科的猫品种列表中的表格。
下面的图片就是我们要实现的设计。够简单了吧?

首先,让我们把我们的猫品种数据提取到一个单独的文件,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作为基本的构建模块。但我们也将我们的ErrorState 和EmptyState 组件从主表中分离出来,确保我们可以在应用程序的其他地方再次使用它们。
最后,整个应用的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>
);
完美,现在我们能够像以前一样显示我们的错误组件了。请看这个代码沙盒的工作实例。
使用这些预期的新功能有几个好处。在它们的帮助下,我们可以通过放置Suspense 和ErrorBoundary 组件来细化控制我们应用程序中的错误和加载状态。
同时,数据获取和渲染逻辑保持解耦(获取在渲染之前启动)。通过Suspense ,还可以避免经常困扰更多传统方法的竞赛条件。
虽然React Suspense本身不是一个数据获取库,但正在采取措施将其整合到现有的解决方案中。
这些新的React更新是非常令人兴奋的,也是非常有趣的玩法!但重要的是要记住,这一切仍然是实验性的,可能会有变化。为了获得最新的信息,你可以关注React 18工作组的讨论。
如果你觉得这篇文章有用,请在Twitter上关注我,并访问我的博客了解更多技术内容。
编码愉快!
The postUI best practices for loading, error, and empty states in Reactappeared first onLogRocket Blog.