Creating a Generic Data Fetching Hook

由于 useGamesuseGenres 两个 hook 的代码高度相似,因此可以抽象出一个通用的获取数据的 hook。

首先将 useGenres 或者 useGames 的代码复制到 hooks 文件夹下新建的 useData.tsx

useData.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { CanceledError } from "axios";
import apiClient from "../services/api-client";
import { useEffect, useState } from "react";

// 删除 Genre 接口,不通用。
interface Genre {
  id: number;
  name: string;
  slug: string;
  image_background: string;
}

// 和 Games 的响应结果类似,保留。
interface FetchGenresResponse {
  count: number;
  results: Genre[];
}

const useGenres = () => {
  const [genres, setGenres] = useState<Genre[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setLoading] = useState(false);
  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);

    apiClient
      .get<FetchGenresResponse>("/genres", { signal: controller.signal })
      .then((res) => {
        setGenres(res.data.results);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        {
          setError(err.message);
          setLoading(false);
        }
      });
    return () => controller.abort();
  }, []);

  return { genres, error, isLoading };
};

export default useGenres;

修改,现在需要接受一个端点作为参数,注意通用类型 T 的标注。

useData.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { CanceledError } from "axios";
import apiClient from "../services/api-client";
import { useEffect, useState } from "react";

interface FetchResponse<T> {
  count: number;
  results: T[];
}

const useData = <T>(endpoint: string) => {
  const [data, setData] = useState<T[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setLoading] = useState(false);
  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);

    apiClient
      .get<FetchResponse>(endpoint, { signal: controller.signal })
      .then((res) => {
        setData(res.data.results);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        {
          setError(err.message);
          setLoading(false);
        }
      });
    return () => controller.abort();
  }, []);

  return { data, error, isLoading };
};

export default useData;

现在对 useGenresuseGames 进行修改,大致就是删去函数体内的代码,转为调用 useData,传入 API 端点,这也是为什么 useData 中把单个对象的类型标注删去的原因。

useGenres.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import useData from "./useData";

interface Genre {
  id: number;
  name: string;
  slug: string;
  image_background: string;
}

const useGenres = () => useData<Genre>("/genres");

export default useGenres;

由于返回的是 data,因此 GenreList 需要修改下变量名称:

GenreList.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import useGenres from "../hooks/useGenres";

const GenreList = () => {
  // const { genres } = useGenres();
  const { data } = useGenres();

  return (
    <div>
      // {genres.map((genre) => (
      {data.map((genre) => (
        <ul key={genre.id}>{genre.name}</ul>
      ))}
    </div>
  );
};

export default GenreList;

同理,useGame 删去了 useData 中的响应类型标注和函数体内的代码,转而使用 useData 传入 API 端点获取响应结果。

useGame.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import useData from "./useData";

export interface Platform {
  id: number;
  name: string;
  slug: string;
}

export interface Game {
  id: number;
  name: string;
  metacritic: number;
  background_image: string;
  parent_platforms: { platform: Platform }[];
}

const useGames = () => useData<Game>("/games");

export default useGames;

因为 useData 返回的对象名称是 data,因此涉及到 GameGrid 调用 useGame 的对象解构。找到 games,按下 F2,修改名称,这样所有同名的地方都会修改了。

GameGrid
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { Text, SimpleGrid, Skeleton } from "@chakra-ui/react";
import useGames from "../hooks/useGames";
import GameCard from "./GameCard";
import GameCardSkeleton from "./GameCardSkeleton";
import GameCardContainer from "./GameCardContainer";

const GameGrid = () => {
  const { data, error, isLoading } = useGames();
  const skeletons = [1, 2, 3, 4, 5, 6];

  return (
    <>
      {error && <Text>{error}</Text>}
      <SimpleGrid
        columns={{ sm: 1, md: 2, lg: 3, xl: 5 }}
        padding="10px"
        spacing={10}
      >
        {isLoading &&
          skeletons.map((Skeleton) => (
            <GameCardContainer>
              <GameCardSkeleton key={Skeleton} />
            </GameCardContainer>
          ))}
        {data.map((game) => (
          <GameCardContainer>
            <GameCard key={game.id} game={game} />
          </GameCardContainer>
        ))}
      </SimpleGrid>
    </>
  );
};

export default GameGrid;

2024-07-21 23:49 2024-08-16 23:25

评论