如果你还不熟悉从API中获取数据的过程或React Hooks,我建议查看这些主题的文章。
现在我们对钩子和数据检索的概念已经很熟悉了,我们可以讨论如何实现我们自己的自定义钩子,专门用于从外部API获取数据。
为什么要实现一个自定义React钩子来获取数据?
与一般的自定义钩子一样,使用它们的主要好处是:
- 可读性
- 封装副作用的能力
- 在多个组件中重复使用逻辑
但除了这些一般的优点外,一个用于获取数据的自定义钩子实现还提供了一个好处,即有一个单一的、可控的方式来处理请求状态,如当一个请求成功完成时(数据已被检索),请求处于加载状态(有一个基线加载状态的视觉提示 - Loader),或请求失败时(总是在控制台记录错误)。
这个好处对于提供直观的用户体验是非常重要的,它让用户知道每当有东西在加载时,总会有一个 "加载中... "的消息显示出来,或者有一个特定的Loader动画,而不是在应用程序的不同区域有不同的方式来处理这个状态(除非这是必须的,那么你总是可以向钩子传递某些参数,以便重载这个行为)。
实用的数据获取钩子实例
假设我们正在构建一个博客应用程序,类似于你正在阅读这篇文章的那个。我们将在应用程序的各个部分与文章进行大量的交互,许多文章部分有类似的设计和类似的行为。
如果你不使用钩子来获取数据,这些部分会看起来像这样:
function App() {
const [requestState, setRequestState] = useState({
data: [],
loading: true,
failed: false,
});
useEffect(() => {
const retrieveArticles = async () => {
try {
const articles = await axios.get("our-endpoint");
setRequestState((prevState) => ({
...prevState,
data: articles,
}));
} catch (err) {
setRequestState((prevState) => ({
...prevState,
failed: true,
}));
} finally {
setRequestState((prevState) => ({
...prevState,
loading: false,
}));
}
};
retrieveArticles();
}, []);
return (
<div className="App">
{requestState.loading ? (
<p>Data is currently loading...</p>
) : requestState.failed ? (
<p>There was an issue loading the articles.</p>
) : (
requestState.data.map((article) => (
<Article title={article.title} body={article.body} />
))
)}
</div>
);
}
正如你在上面的例子中所看到的,这是一个相当直接的在React中获取数据的方法的实现。然而,如果我们想做完全相同的请求,或者使用相同的配置,但使用不同的URL会发生什么?
嗯,你已经猜到了!我们必须重新做一遍,这并不是处理这种情况的最方便的方法,特别是如果应用程序在未来会大量增长。
那么,我们如何缓解这一过程呢?这就对了,钩子现在要发挥作用了。让我们看看下面这个带有钩子的再现的例子。
import axios from "axios";
import { useEffect, useState } from "react";
const useFetchData = (
requestConfig, // Axios request config
) => {
const localRequestConfig = requestConfig || {};
const [state, setState] = useState({
loading: true,
data: null,
error: null,
});
if (!localRequestConfig?.method) {
localRequestConfig.method = 'GET';
}
useEffect(() => {
if (localRequestConfig.url) {
axios(localRequestConfig)
.then((res) => {
setState(prevState => ({
...prevState,
data: res.data,
}))
})
.catch((err) => {
setState(prevState => ({
...prevState,
error: err,
}))
})
.finally(() => {
setState(prevState => ({
...prevState,
loading: false,
}))
});
} else {
setState(prevState => ({
...prevState,
loading: false,
error: new Error('No URL provided!'),
}));
}
return state;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [requestConfig]);
};
export default useFetchData;
现在我们已经创建了钩子,让我们也替换掉之前的实现,并将其添加到我们的应用程序中。
function App() {
const {
data: articles,
loading: articlesLoading,
error: articlesError,
} = useFetchData();
return (
<div className="App">
{articlesLoading ? (
<p>Data is currently loading...</p>
) : articlesError ? (
<p>There was an issue loading the articles.</p>
) : (
articles.map((article) => (
<Article title={article.title} body={article.body} />
))
)}
</div>
);
}
所以,正如你所看到的,我们现在用一种更好的方式来重新利用API调用逻辑,而且没有遗漏任何功能。通过为钩子提供requestConfig参数,我们进一步提高了可重用性,因为用户可以简单地传递他需要的配置,仅此而已。没有更多的麻烦;它是简单的即插即用。
现在,如果我们需要在其他10个组件中使用API调用逻辑,我们就不需要在这里复制所有这块内容。
const [requestState, setRequestState] = useState({
data: [],
loading: true,
failed: false,
});
useEffect(() => {
const retrieveArticles = async () => {
try {
const articles = await axios.get("our-endpoint");
setRequestState((prevState) => ({
...prevState,
data: articles,
}));
} catch (err) {
setRequestState((prevState) => ({
...prevState,
failed: true,
}));
} finally {
setRequestState((prevState) => ({
...prevState,
loading: false,
}));
}
};
retrieveArticles();
}, []);
这个片段就是我们需要复制的全部内容:
const {
data: articles,
loading: articlesLoading,
error: articlesError,
} = useFetchData();
Voila!现在我们已经实现了一个处理数据获取过程的钩子,我们只需复制5行代码,就可以在任何我们喜欢的地方重用它。当然,如果你确实需要扩展这个功能来处理其他情况,你可以简单地对钩子进行参数化,这样你就不会遇到任何不足之处。
只是为了让你有一个更好的视角,我们已经设法将组件文件的大小减少了这么多。

在使用钩子重构你的代码库之后,一个好的下一步是对你的请求进行缓存,以节省资源和提高性能。你可以在这里阅读我的另一篇文章,帮助你了解如何使用:https://upmostly.com/tutorials/how-to-integrate-cache-in-react-applications
最后的话
我希望你通过阅读这篇文章学到了一些新东西,并希望你喜欢这篇文章。下一篇再见
此外,如果你有任何建议、问题或提示要分享,请随时在下面留言。这将是非常感激的。
干杯!