发布于 2026-01-06 0 阅读
0

使用钩子加载和显示数据

使用钩子加载和显示数据

在本系列文章中,我们不使用状态管理库或提出一刀切的解决方案,而是从最基本的功能开始,根据需要逐步构建状态管理。


  • 在本文中,我们将介绍如何使用钩子加载和显示数据。
  • 在第二篇文章中,我们将学习如何使用钩子更改远程数据。
  • 在第三篇文章中,我们将看到如何使用 React Context 在组件之间共享数据,而无需使用全局变量、单例或借助 MobX 或 Redux 等状态管理库。
  • 在第四篇文章中,我们将看到如何使用SWR在组件之间共享数据,这可能是我们一开始就应该做的。

最终代码可以在这个GitHub 仓库中找到。它是 TypeScript 代码,但类型注解非常简洁。另外请注意,这并非生产代码。为了专注于状态管理,许多其他方面(例如依赖倒置、测试或优化)并未考虑在内。

使用钩子加载数据

假设我们有一个 REST API,其中包含Commodore 64游戏列表。我的意思是,为什么不呢?

需求:我们需要加载列表并显示游戏。

我最喜欢的 Commodore 64 游戏

1. 基本获取

以下是我们从服务器获取游戏列表的方法:

const getGames = () => {
  return fetch('http://localhost:3001/games/').then(response => response.json());
};
Enter fullscreen mode Exit fullscreen mode

我们可以将其用于 React 应用中。我们的第一个版本如下所示:

App.tsx(由 index.tsx 渲染)(参见仓库

import React from 'react';

const getGames = () => {
  return fetch('http://localhost:3001/games/').then(response => response.json());
};

export const App = () => {
  const [games, setGames] = React.useState([]);

  React.useEffect(() => {
    getGames().then(games => setGames(games));
  }, []);

  return <pre>{JSON.stringify(games, null, 2)}</pre>;
};
Enter fullscreen mode Exit fullscreen mode

在组件首次渲染时Appgames数组为空。然后,当 Promise 返回的结果getGames解析后,games数组将包含所有游戏,并以非常基础的方式显示出来。

2. 自定义 React Hook

我们可以轻松地将其提取到单独文件中的自定义 React Hook 中。

useGames.ts参见仓库

import React from 'react';

const getGames = () => {
  return fetch('http://localhost:3001/games/').then(response => response.json());
};

export const useGames = () => {
  const [games, setGames] = React.useState([]);

  React.useEffect(() => {
    getGames().then(games => setGames(games));
  }, []);

  return games;
};
Enter fullscreen mode Exit fullscreen mode

App.tsx参见仓库

import React from 'react';
import { useGames } from './useGames';

export const App = () => {
  const games = useGames();
  return <pre>{JSON.stringify(games, null, 2)}</pre>;
};
Enter fullscreen mode Exit fullscreen mode

3. 处理错误和待处理状态

我们的自定义钩子无法处理待处理和错误状态。从服务器加载数据时没有任何视觉反馈,更糟糕的是:加载失败时也没有任何错误信息。如果服务器宕机,游戏列表将保持为空,且不会显示任何错误信息。

我们可以解决这个问题。有很多库可以实现这个功能,最流行的是react-async;但我现在还不想添加依赖项。我们来看看处理错误和待处理状态所需的最少代码是什么。

使用异步函数

我们编写了一个自定义钩子,它接受一个异步函数(返回一个 Promise)和一个默认值。

这个钩子返回一个包含 3 个元素的元组:[value, error, isPending]。它会调用异步函数一次*,并在解析时更新值,当然,除非出现错误。

function useAsyncFunction<T>(asyncFunction: () => Promise<T>, defaultValue: T) {
  const [state, setState] = React.useState({
    value: defaultValue,
    error: null,
    isPending: true
  });

  React.useEffect(() => {
    asyncFunction()
      .then(value => setState({ value, error: null, isPending: false }))
      .catch(error => setState({ ...state, error: error.toString(), isPending: false }));
  }, [asyncFunction]); // *

  const { value, error, isPending } = state;
  return [value, error, isPending];
}
Enter fullscreen mode Exit fullscreen mode

*内部代码调用异步函数一次,然后在每次值发生变化时调用一次。更多详情请参阅:使用状态钩子使用效果钩子钩子 API 参考useEffectuseAsyncFunctionasyncFunction

现在在 useGames.ts 中,我们可以直接使用这个新的自定义钩子,将getGames函数和空数组的初始值作为参数传递。

...
export const useGames = () => {
  const games = useAsyncFunction(getGames, []); // 🤔 new array on every render?
  return games;
};
Enter fullscreen mode Exit fullscreen mode

不过这里有个小问题。每次useGames调用 `get()` 方法时,我们都会传递一个新的空数组,而每次App组件渲染时都会调用 `get()` 方法。这会导致每次渲染时数据都被重新获取,但每次获取数据都会触发新的渲染,从而导致无限循环。

我们可以通过将初始值存储在钩子函数外部的常量中来避免这种情况:

...
const emptyList = [];

export const useGames = () => {
  const [games] = useAsyncFunction(getGames, emptyList);
  return games;
};
Enter fullscreen mode Exit fullscreen mode

TypeScript 小插曲

如果您使用的是纯 JavaScript,则可以跳过此部分。

如果您使用的是严格 TypeScript,由于使用了“noImplicitAny”编译器选项,上述代码将无法正常工作。这是因为 ` const emptyList = [];is` 隐式地是一个数组any

我们可以像这样添加注解const emptyList: any[] = [];然后继续。但我们使用 TypeScript 是有原因的。这种显式定义any可以(也应该)更具体。

这个列表包含什么内容?游戏!这是一份游戏列表。

const emptyList: Game[] = [];
Enter fullscreen mode Exit fullscreen mode

当然,现在我们需要定义一个Game类型。不过别担心!我们已经从服务器收到了 JSON 响应,其中每个游戏对象看起来都像这样:

{
  "id": 5,
  "title": "Kung-Fu Master",
  "year": 1984,
  "genre": "beat'em up",
  "url": "https://en.wikipedia.org/wiki/Kung-Fu_Master_(video_game)",
  "status": "in-progress",
  "img": "http://localhost:3001/img/kung-fu-master.gif"
}
Enter fullscreen mode Exit fullscreen mode

我们可以使用transform.tools将其转换为 TypeScript 接口(或类型)。

type Game = {
  id: number;
  title: string;
  year: number;
  genre: string;
  url: string;
  status: 'not-started' | 'in-progress' | 'finished';
  img: string;
};
Enter fullscreen mode Exit fullscreen mode
还有一件事:

我们声明useAsyncFunction返回的是一个元组,但 TypeScript 的类型推断(@3.6.2)无法理解这一点。它将返回类型推断为 `T` Array<(boolean | Game[] | null)>。我们可以显式地将函数的返回类型标注为 `T`,其中 ` [T, string | null, boolean]T`T是 `T` 的(泛型)类型value, `T` 是 `T` 的类型,` T`(string | null)`T` 的属性errorbooleanisPending

export function useAsyncFunction<T>(
  asyncFunction: () => Promise<T>,
  defaultValue: T
): [T, string | null, boolean] {
  ...
}
Enter fullscreen mode Exit fullscreen mode

现在当我们使用该函数时,TypeScript 会建议正确的类型。

const [games] = useAsyncFunction(getGames, emptyList); // games is of type Game[]
Enter fullscreen mode Exit fullscreen mode

TypeScript 插曲结束。

编写我们定制的钩子

useAsyncFunction.ts现在看起来像这样:(参见仓库

import React from 'react';

export function useAsyncFunction<T>(
  asyncFunction: () => Promise<T>,
  defaultValue: T
): [T, string | null, boolean] {
  const [state, setState] = React.useState({
    value: defaultValue,
    error: null,
    isPending: true
  });

  React.useEffect(() => {
    asyncFunction()
      .then(value => setState({ value, error: null, isPending: false }))
      .catch(error =>
        setState({ value: defaultValue, error: error.toString(), isPending: false })
      );
  }, [asyncFunction, defaultValue]);

  const { value, error, isPending } = state;
  return [value, error, isPending];
}
Enter fullscreen mode Exit fullscreen mode

我们在useGames钩子函数中使用它:

useGames.ts参见仓库

import { useAsyncFunction } from './useAsyncFunction';

const getGames = (): Promise<Game[]> => {
  return fetch('http://localhost:3001/games/').then(response => response.json());
};

type Game = {
  id: number;
  title: string;
  year: number;
  genre: string;
  url: string;
  status: 'not-started' | 'in-progress' | 'finished';
  img: string;
};

const emptyList: Game[] = [];

export const useGames = () => {
  const [games] = useAsyncFunction(getGames, emptyList);
  return games;
};
Enter fullscreen mode Exit fullscreen mode

更改用户界面以显示错误和待处理状态

太好了!但是我们仍然没有处理错误和待处理状态。我们需要修改我们的App组件:

import React from 'react';
import { useGames } from './useGames';

export const App = () => {
  const { games, error, isPending } = useGames();

  return (
    <>
      {error && <pre>ERROR! {error}...</pre>}
      {isPending && <pre>LOADING...</pre>}
      <pre>{JSON.stringify(games, null, 2)}</pre>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

我们的useGames钩子应该返回一个包含三个键的对象games,,errorisPending

export const useGames = () => {
  const [games, error, isPending] = useAsyncFunction(getGames, emptyList);
  return { games, error, isPending };
};
Enter fullscreen mode Exit fullscreen mode

我们也在改进getGames处理除 200 以外的 HTTP 状态码的功能,将其视为错误:

const getGames = (): Promise<Game[]> => {
  return fetch('http://localhost:3001/games/').then(response => {
    if (response.status !== 200) {
      throw new Error(`${response.status} ${response.statusText}`);
    }
    return response.json();
  });
};
Enter fullscreen mode Exit fullscreen mode

我们目前的代码如下所示:(参见仓库)。

结论

我们已经了解了如何使用 React hooks 从 REST API 加载数据。

下一篇文章我们将了解如何使用 HTTPPATCH请求更改远程数据,以及如何在请求成功时更新客户端数据。

资源

延伸阅读:

文章来源:https://dev.to/juliang/loading-and-displaying-data-with-hooks-jlj