R

React Hook 数据抓取 问题 useFetch Hook 深入探讨 Connect

2025-05-25

React Hook 数据获取

问题

钩子useFetch

更进一步

连接

对于大多数单页应用来说,发起 HTTP 请求是一项常见任务。由于网络请求的异步特性,我们需要在请求的生命周期内管理其状态:启动、加载阶段,以及最终的响应处理或错误处理(如果发生)。

问题

如今,创建新的React.js Web 应用时,不再使用任何外部状态管理库(例如 Redux),而仅依赖 React State 和 React Context 的做法越来越普遍。自React.js 16.8发布以来,这种趋势更加明显,因为 Hooks 的引入简化了 Context API,使其对开发者更具吸引力。
在这种 Web 应用中,一个发出网络请求的 React 组件可能如下所示。

import * as React from "react"
import { topicsURL } from "./api"

function TopicsList() {
  const [topics, setTopics] = React.useState([])
  const [loading, setLoading] = React.useState(false)
  const [error, setError] = React.useState(null)
  React.useEffect(() => {
    setLoading(true)
    fetch(topicsURL)
      .then(response => {
        if (!response.ok) {
          throw new Error("Request failed")
        }
        return response.json()
      })
      .then(data => setTopics(data))
      .catch(e => setError(e))
      .finally(() => setLoading(false))
  }, [])

  if (error) {
    return <div>An error has occurred: {error.message}</div>
  }
  if (loading) {
    return <div>Loading...</div>
  }
  return (
    <ul>
      {topics.map(topic => (
        <li key={topic.id}>
          <a href={topic.url}>{topic.title}</a>;
        </li>
      ))}
    </ul>
  )
}
Enter fullscreen mode Exit fullscreen mode

TopicsList组件相当不错,但其大部分代码都用于管理网络请求,掩盖了其真正用途:显示主题列表。这似乎存在关注点分离的问题。
此外,相同的代码会在许多其他组件中重复,仅修改请求 URL。每个组件都会声明三个状态变量,在 effect 中发出请求,管理加载状态,并仅在请求成功时有条件地渲染组件。
最后,请求状态取决于三个变量(topicsloadingerror)的值。如果以错误的顺序检查这些变量,很容易把事情搞砸。为了更好地理解这个问题,请参阅文章:停止使用 isLoading 布尔值

钩子useFetch

我们可以通过定义一个管理网络请求的自定义钩子来解决前面描述的问题。我们的目标是:

  1. 避免重写逻辑来管理请求。
  2. 将请求管理代码与渲染分离。
  3. 以原子方式处理请求状态。
import * as React from "react"

const reducer = (state, action) => {
  switch (action.type) {
    case "loading":
      return {
        status: "loading",
      }
    case "success":
      return {
        status: "success",
        data: action.data,
      }
    case "error":
      return {
        status: "error",
        error: action.error,
      }
    default:
      return state
  }
}

export function useFetch(url) {
  const [state, dispatch] = React.useReducer(reducer, { status: "idle" })
  React.useEffect(() => {
    let subscribed = true
    dispatch({ type: "loading" })
    fetch(url)
      .then(response => {
        if (!response.ok) {
          throw new Error("Request failed")
        }
        return response.json()
      })
      .then(data => {
        if (subscribed) {
          dispatch({ type: "success", data })
        }
      })
      .catch(error => {
        if (subscribed) {
          dispatch({ type: "error", error })
        }
      })
    return () => {
      subscribed = false
    }
  }, [url])
  return state
}
Enter fullscreen mode Exit fullscreen mode

这个useFetch钩子是一个很有用的抽象,它可以轻松地在应用程序的各个组件之间共享。请求状态取决于单个status变量,而不是三个。subscribed当 unmount 事件在请求完成之前发生时,该变量可以阻止对已卸载组件进行组件更新。
没有人乐意在浏览器控制台中看到这样的警告。

警告:无法在已卸载的组件上调用 setState(或 forceUpdate)。这是一个无操作,但它表示您的应用存在内存泄漏。要修复此问题,请在 componentWillUnmount 方法中取消所有订阅和异步任务。

使用钩子

有了useFetch钩子之后TopicsList组件就变成这样了。

import { useFetch, topicsURL } from "./api"

function TopicsList() {
  const res = useFetch(topicsURL)
  return (
    <>
      {res.status === "loading" && <div>Loading...</div>}
      {res.status === "error" && (
        <div>An error has occurred: {res.error.message}</div>
      )}
      {status === "success" && (
        <ul>
          {res.data.map(topic => (
            <li key={topic.id}>
              <a href={topic.url}>{topic.title}</a>
            </li>
          ))}
        </ul>
      )}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

由于清晰地定义了组件的用途,代码可读性更高。现在,渲染逻辑与请求管理分离,并且不再存在混合的抽象层级。

福利#1:TypeScript 版本

对于类型安全爱好者(我在这里✋),这是 TypeScript 版本。

import * as React from "react"

export type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error }

export type RequestAction<T> =
  | { type: "start" }
  | { type: "completed"; data: T }
  | { type: "failed"; error: Error }

export function useFetch<T>(route: string): RequestState<T> {
  const [state, dispatch] = React.useReducer<
    React.Reducer<RequestState<T>, RequestAction<T>>
  >(reducer, { status: "idle" })
  React.useEffect(() => {
    let subscribed = true
    if (route) {
      dispatch({ type: "start" })
      fetch(route)
        .then(response => {
          if (!response.ok) {
            throw new Error("Request failed")
          }
          return response.json()
        })
        .then(data => {
          if (subscribed) {
            dispatch({ type: "completed", data })
          }
        })
        .catch(error => {
          if (subscribed) {
            dispatch({ type: "failed", error })
          }
        })
    }
    return () => {
      subscribed = false
    }
  }, [route])
  return state
}

export function reducer<T>(
  state: RequestState<T>,
  action: RequestAction<T>
): RequestState<T> {
  switch (action.type) {
    case "start":
      return {
        status: "loading",
      }
    case "completed":
      return {
        status: "success",
        data: action.data,
      }
    case "failed":
      return {
        status: "error",
        error: action.error,
      }
    default:
      return state
  }
}
Enter fullscreen mode Exit fullscreen mode

那么,为每个请求定义一个具有适当类型的辅助函数可能会很有用,而不是直接在组件中使用钩子。主题请求应该像这样。

function useTopics(): RequestState<Topic[]> {
  return useFetch(topicsURL)
}
Enter fullscreen mode Exit fullscreen mode

Union 类型强制我们在访问任何其他属性之前检查响应的状态。res.data只有当语言确定在同一作用域内状态为“成功”时才允许写入。因此,多亏了 TypeScript,我们可以避免类似的错误Uncaught TypeError: Cannot read property 'map' of undefined

 福利#2:测试技巧

钩子useFetch可以帮助我们简化单元测试。实际上,我们可以监视钩子并返回一个合适的测试替身。由于钩子监视隐藏了获取请求的异步行为,直接提供响应,组件的测试变得更加容易。
存根让我们能够推断组件行为和测试预期,而无需担心异步执行。
假设使用Jest测试库,主题列表组件的单元测试可能如下所示。

import * as React from "react"
import { render, screen } from "@testing-library/react"
import TopicsList from "../TopicsList"
import * as api from "../api"

const testData = Array.from(Array(5).keys(), index => ({
  id: index,
  title: `Topic ${index}`,
  url: `https://example.com/topics/${index}`,
}))

test("Show a list of topic items", () => {
  jest.spyOn(api, "useTopics").mockReturnValue({
    status: "success",
    data: testData,
  })
  render(<TopicsList />)
  expect(screen.getAllByRole("listitem")).toHaveLength(testData.length)
})
Enter fullscreen mode Exit fullscreen mode

即使在测试中模拟获取请求有其他方法,也请停止模拟获取,这种方法在设置异步单元测试很棘手的复杂情况下很有用。

更进一步

useFetch 钩子是一个方便的实用程序,用于从服务器检索数据并管理网络请求。它足够简单但功能强大。无论如何,它并非适用于所有用例,我在这里提供一些注意事项。

  • 自定义钩子可以轻松修改,以用于任何异步任务,即每个函数都返回一个Promise。例如,它的签名可以如下所示。
function useAsync<T>(task: Promise<T> | () => Promise<T>): AsyncState<T>`
Enter fullscreen mode Exit fullscreen mode
  • 用Axios替换原生的 fetch 方法非常简单。只需要删除检查响应是否成功以及解析 JSON 响应体的代码,因为 Axios 会在内部完成这些工作。
  • 如果 API 端点需要一些标头,例如Authorization,您可以定义一个自定义客户端函数,使用所需的标头增强获取请求,并用该客户端替换获取。
  • 在复杂的 Web 应用程序中,发出大量网络请求,需要缓存等高级功能,最好使用强大的 React 数据同步库React Query 。

连接

你觉得它有用吗?你还有什么疑问吗?欢迎评论或联系我。你可以在 Twitter 上联系我:@mircobellaG

文章来源:https://dev.to/mbellagamba/data-fetching-react-hook-4dfc
PREV
面向开发人员的 7 个微服务最佳实践 微服务最佳实践 不要忘记您的安全带……
NEXT
为什么事后测试是一种不好的做法