如何创建自定义 React Hook 来获取和缓存数据

已发表: 2022-03-10
快速总结↬ React 应用程序中的许多组件很有可能必须调用 API 来检索将显示给用户的数据。 使用componentDidMount()生命周期方法已经可以做到这一点,但是随着 Hooks 的引入,您可以构建一个自定义钩子来为您获取和缓存数据。 这就是本教程将介绍的内容。

如果你是 React Hooks 的新手,你可以从查看官方文档开始了解它。 之后,我建议阅读 Shedrack Akintayo 的“React Hooks API 入门”。 为确保您继续跟进,还有一篇由 Adeneye David Abiodun 撰写的文章,其中涵盖了 React Hooks 的最佳实践,我相信这对您很有用。

在本文中,我们将使用 Hacker News Search API 来构建一个自定义的钩子,我们可以使用它来获取数据。 虽然本教程将介绍 Hacker News Search API,但我们将让钩子以某种方式工作,它将从我们传递给它的任何有效API 链接返回响应。

最佳反应实践

React 是一个很棒的 JavaScript 库,用于构建丰富的用户界面。 它提供了一个很好的组件抽象,用于将您的接口组织成功能良好的代码,并且您可以使用它来做任何事情。 阅读有关 React 的相关文章 →

在 React 组件中获取数据

在 React hooks 之前,通常在 componentDidMount() 生命周期方法中获取初始数据,并在componentDidMount() componentDidUpdate()周期方法中获取基于 prop 或状态变化的数据。

以下是它的工作原理:

 componentDidMount() { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=JavaScript` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } componentDidUpdate(previousProps, previousState) { if (previousState.query !== this.state.query) { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${this.state.query}` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } }

一旦组件被挂载, componentDidMount生命周期方法就会被调用,完成后,我们所做的就是通过 Hacker News API 发出搜索“JavaScript”的请求,并根据响应更新状态。

另一方面, componentDidUpdate生命周期方法在组件发生更改时被调用。 我们将状态中的前一个查询与当前查询进行了比较,以防止每次我们在状态中设置“数据”时调用该方法。 我们从使用钩子中得到的一件事是以一种更简洁的方式组合这两种生命周期方法——这意味着我们不需要在组件挂载和更新时使用两种生命周期方法。

跳跃后更多! 继续往下看↓

使用useEffect Hook 获取数据

一旦组件被挂载, useEffect钩子就会被调用。 如果我们需要根据某些 prop 或状态更改重新运行钩子,我们需要将它们传递给依赖数组(这是useEffect钩子的第二个参数)。

让我们探索如何使用钩子获取数据:

 import { useState, useEffect } from 'react'; const [status, setStatus] = useState('idle'); const [query, setQuery] = useState(''); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]);

在上面的示例中,我们将query作为依赖项传递给我们的useEffect挂钩。 通过这样做,我们告诉useEffect跟踪查询更改。 如果先前的query值与当前值不同,则再次调用useEffect

话虽如此,我们还根据需要在组件上设置了几个status ,因为这将更好地根据一些有限状态status向屏幕传达一些信息。 在空闲状态下,我们可以让用户知道他们可以使用搜索框开始。 在fetching状态下,我们可以显示一个spinner 。 而且,在获取状态下,我们将渲染数据。

在尝试将状态设置为fetched之前设置数据非常重要,这样您可以防止在设置fetched状态时由于数据为空而发生闪烁。

创建自定义挂钩

“自定义钩子是一个 JavaScript 函数,其名称以 'use' 开头,并且可以调用其他钩子。”

— 反应文档

这就是它的本质,并且与 JavaScript 函数一起,它允许您在应用程序的多个部分中重用某些代码。

React Docs 中的定义已经给出了它,但让我们看看它在实践中是如何使用计数器自定义钩子的:

 const useCounter = (initialState = 0) => { const [count, setCount] = useState(initialState); const add = () => setCount(count + 1); const subtract = () => setCount(count - 1); return { count, add, subtract }; };

在这里,我们有一个常规函数,我们接受一个可选参数,将值设置为我们的状态,并添加可用于更新它的addsubtract方法。

在我们的应用程序中需要计数器的任何地方,我们都可以像常规函数一样调用useCounter并传递一个initialState ,以便我们知道从哪里开始计数。 当我们没有初始状态时,我们默认为 0。

以下是它在实践中的工作方式:

 import { useCounter } from './customHookPath'; const { count, add, subtract } = useCounter(100); eventHandler(() => { add(); // or subtract(); });

我们在这里所做的是从我们声明它的文件中导入我们的自定义钩子,以便我们可以在我们的应用程序中使用它。 我们将它的初始状态设置为 100,所以每当我们调用add()时,它的count就会count 1,而每当我们调用subtract()时,它就会减少 1。

创建useFetch钩子

现在我们已经学会了如何创建一个简单的自定义钩子,让我们提取我们的逻辑来获取数据到一个自定义钩子中。

 const useFetch = (query) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]); return { status, data }; };

除了它是一个接受query并返回statusdata的函数之外,这与我们上面所做的几乎相同。 而且,这是一个useFetch钩子,我们可以在 React 应用程序的多个组件中使用它。

这行得通,但现在这个实现的问题是,它是 Hacker News 特有的,所以我们可以直接称它为useHackerNews 。 我们打算做的是,创建一个可用于调用任何 URL 的useFetch钩子。 让我们修改它以接收 URL!

 const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch(url); const data = await response.json(); setData(data); setStatus('fetched'); }; fetchData(); }, [url]); return { status, data }; };

现在,我们的 useFetch 钩子是通用的,我们可以在各种组件中随意使用它。

这是消费它的一种方式:

 const [query, setQuery] = useState(''); const url = query && `https://hn.algolia.com/api/v1/search?query=${query}`; const { status, data } = useFetch(url);

在这种情况下,如果query的值为truthy ,我们继续设置 URL,如果不是,我们可以传递 undefined,因为它会在我们的钩子中处理。 无论如何,效果都会尝试运行一次。

记忆获取的数据

记忆化是一种技术,如果我们在某个初始阶段发出某种请求来获取它,我们将使用它来确保我们不会到达hackernews端点。 存储昂贵的 fetch 调用的结果将为用户节省一些加载时间,从而提高整体性能。

注意有关更多上下文,您可以查看 Wikipedia 对 Memoization 的解释。

让我们探索一下如何做到这一点!

 const cache = {}; const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache[url]) { const data = cache[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

在这里,我们将 URL 映射到他们的数据。 因此,如果我们请求获取一些现有数据,我们从本地缓存中设置数据,否则,我们继续发出请求并将结果设置在缓存中。 这确保了当我们在本地获得可用数据时,我们不会进行 API 调用。 我们还会注意到,如果 URL 是falsy ,我们会取消效果,因此它确保我们不会继续获取不存在的数据。 我们不能在useEffect钩子之前这样做,因为这将违反钩子的规则之一,即始终在顶层调用钩子。

在不同的范围内声明cache是可行的,但它使我们的钩子违背了纯函数的原则。 此外,当我们不再想使用该组件时,我们还希望确保 React 有助于清理我们的烂摊子。 我们将探索useRef来帮助我们实现这一目标。

使用useRef记忆数据

useRef就像一个盒子,可以在其.current property中保存一个可变值。”

— 反应文档

使用useRef ,我们可以轻松地设置和检索可变值​​,并且它的值在组件的整个生命周期中都保持不变。

让我们用一些useRef魔法来替换我们的缓存实现!

 const useFetch = (url) => { const cache = useRef({}); const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache.current[url]) { const data = cache.current[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

在这里,我们的缓存现在位于我们的useFetch挂钩中,其中一个空对象作为初始值。

包起来

好吧,我确实说过在设置获取状态之前设置数据是一个好主意,但是我们也可能遇到两个潜在的问题:

  1. 当我们处于获取状态时,我们的单元测试可能会因为数据数组不为空而失败。 React 实际上可以批处理状态更改,但如果它是异步触发的,它就不能这样做;
  2. 我们的应用程序重新渲染的次数超出了应有的次数。

让我们对我们的useFetch挂钩进行最后的清理。我们将首先将我们的useState s 切换为useReducer 。 让我们看看它是如何工作的!

 const initialState = { status: 'idle', error: null, data: [], }; const [state, dispatch] = useReducer((state, action) => { switch (action.type) { case 'FETCHING': return { ...initialState, status: 'fetching' }; case 'FETCHED': return { ...initialState, status: 'fetched', data: action.payload }; case 'FETCH_ERROR': return { ...initialState, status: 'error', error: action.payload }; default: return state; } }, initialState);

在这里,我们添加了一个初始状态,它是我们传递给每个单独的useState的初始值。 在我们的useReducer中,我们检查我们想要执行的操作类型,并根据它设置适当的值来状态。

这解决了我们之前讨论的两个问题,因为我们现在可以同时设置状态和数据,以帮助防止不可能的状态和不必要的重新渲染。

还剩下一件事:清理我们的副作用。 Fetch 实现了 Promise API,因为它可以被解决或拒绝。 如果我们的钩子试图在组件卸载时进行更新,因为某些Promise刚刚得到解决,React 将返回Can't perform a React state update on an unmounted component.

让我们看看如何通过useEffect清理来解决这个问题!

 useEffect(() => { let cancelRequest = false; if (!url) return; const fetchData = async () => { dispatch({ type: 'FETCHING' }); if (cache.current[url]) { const data = cache.current[url]; dispatch({ type: 'FETCHED', payload: data }); } else { try { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; if (cancelRequest) return; dispatch({ type: 'FETCHED', payload: data }); } catch (error) { if (cancelRequest) return; dispatch({ type: 'FETCH_ERROR', payload: error.message }); } } }; fetchData(); return function cleanup() { cancelRequest = true; }; }, [url]);

在这里,我们将cancelRequest在效果内定义后设置为true 。 因此,在我们尝试进行状态更改之前,我们首先确认组件是否已卸载。 如果已卸载,我们跳过更新状态,如果尚未卸载,我们更新状态。 这将解决React 状态更新错误,并防止我们组件中的竞争条件。

结论

我们已经探索了几个钩子概念来帮助在我们的组件中获取和缓存数据。 我们还清理了我们的useEffect钩子,这有助于防止我们的应用程序中出现大量问题。

如果您有任何疑问,请随时将它们放在下面的评论部分!

  • 请参阅本文的回购 →

参考

  • “介绍 Hooks”,React 文档
  • “React Hooks API 入门”,Shedrack Akintayo
  • “React Hooks 的最佳实践”,Adeneye David Abiodun
  • “函数式编程:纯函数”,Arne Brasseur