【React】 如何在同一个 页面中执行不同的 hook

52 阅读2分钟

场景:在 React 中封装不同的 hook,难免 hooks 中有些逻辑相同,有些部分不同,但是 React 中会有个限制,就是 hook 必须在顶端执行,不能用条件语句去判断。具体来说,我有这样一个需求,我将无限滚动加载的逻辑封装在一个 hook 里面,但是每个用到无限滚动加载的页面,仅是接口请求不一样、缓存的 key 不一样, 我封装了多个 hook,而无限滚动加载的逻辑封装在一个 hook 里面

代码展现

这是一个文章列表的无限下拉加载展示代码,对于其他页面的下拉加载,只有 useInfiniteQueryqueryKeyqueryFn 不一样

// useArticleList.ts
import { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import { PostPaginated, PostParams } from "@/types/article";
import { useInfiniteQuery } from "@tanstack/react-query";
import { articleKeys } from "./request/articleKeys";
import { articleService } from "@/services/article";

type ArticleListType = "all" | "category" | "tag" | "date";

interface UseArticleListOptions {
  type: ArticleListType;
  params?: PostParams;
}

export function useArticleList({
  params = {},
}: UseArticleListOptions) {
  const { ref, inView } = useInView();

  const {
    data,
    isLoading,
    error,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage
  } = useInfiniteQuery<PostPaginated>({
    queryKey: articleKeys.lists(),
    queryFn: async ({ pageParam }) => {
      const response = await articleService.getArticles({
        ...params,
        cursor: pageParam as string | undefined,
        limit: 10,
      });
      return response.data;
    },
    initialPageParam: undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

  useEffect(() => {
    if (inView && hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage, fetchNextPage]);

  const articles = data?.pages.flatMap((page) => page.content) ?? [];

  return {
    articles,
    isLoading,
    error,
    loadMoreProps: {
      hasNextPage,
      isFetchingNextPage,
      loadMoreRef: ref,
    },
  };
}

最好的办法是 queryFn queryKey 接受传入, 当然我这里是要体现 同一个 hook 中执行不同的 hook, 所以抽离了多个 hook

// article.ts
export function useInfiniteArticles(options: PostParams = {}) {
  return useInfiniteQuery<PostPaginated>({
    queryKey: articleKeys.lists(),
    queryFn: async ({ pageParam }) => {
      const response = await articleService.getArticles({
        ...options,
        cursor: pageParam as string | undefined,
        limit: 10,
      });
      return response.data;
    },
    initialPageParam: undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });
}

export function useInfiniteArticlesByCategory(options: PostParams = {}) {
  return useInfiniteQuery<PostPaginated>({
    queryKey: articleKeys.category(options.categoryId!),
    queryFn: async ({ pageParam }) => {
      const response = await articleService.getArticlesByCategoryId(
        options.categoryId!,
        {
          cursor: pageParam as string | undefined,
          limit: 10,
        },
      );
      return response.data;
    },
    initialPageParam: undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });
}

然后在 useArticle.ts 根据条件的不同执行不同的 hook

import { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import {
  useInfiniteArticles,
  useInfiniteArticlesByCategory,
} from "./request/article";
import { PostParams } from "@/types/article";

type ArticleListType = "all" | "category" | "tag" | "date";

interface UseArticleListOptions {
  type: ArticleListType;
  params?: PostParams;
}

export function useArticleList({
  type = "all",
  params = {},
}: UseArticleListOptions) {
  const { ref, inView } = useInView();
 // 关键代码在此!!!
  const queryHook = (() => {
    switch (type) {
      case "category":
        // eslint-disable-next-line react-hooks/rules-of-hooks
        return useInfiniteArticlesByCategory(params);
      default:
        // eslint-disable-next-line react-hooks/rules-of-hooks
        return useInfiniteArticles(params);
    }
  })();

  const {
    data,
    isLoading,
    error,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage,
  } = queryHook;

  useEffect(() => {
    if (inView && hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage, fetchNextPage]);

  const articles = data?.pages.flatMap((page) => page.content) ?? [];

  return {
    articles,
    isLoading,
    error,
    loadMoreProps: {
      hasNextPage,
      isFetchingNextPage,
      loadMoreRef: ref,
    },
  };
}

最主要的代码是 queryhook 这个立即执行方法, 在这个方法里面可以根据不同的条件执行不同的 hook

 const queryHook = (() => {
    switch (type) {
      case "category":
        // eslint-disable-next-line react-hooks/rules-of-hooks
        return useInfiniteArticlesByCategory(params);
      case "tag":
        // eslint-disable-next-line react-hooks/rules-of-hooks
        return useInfiniteArticlesByTag(params);
      case "date":
        // eslint-disable-next-line react-hooks/rules-of-hooks
        return useInfiniteArticlesByDate(params);
      default:
        // eslint-disable-next-line react-hooks/rules-of-hooks
        return useInfiniteArticles(params);
    }
  })();

刚刚说到仅仅是 querykey 和 queryFn 不同,没有必要再次封装 hooks,参数传入就行

// useArticleList.ts
export function useArticleList({
  queryKey,
  queryFn
}: UseArticleListOptions) {
  const { ref, inView } = useInView();
  const {
    data,
    isLoading,
    error,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery<PostPaginated>({
    queryKey: [queryKey],
    queryFn: async ({ pageParam }) => {
      const response = await queryFn({
        cursor: pageParam as string | undefined,
        limit: 10,
      });
      return response;
    },
    initialPageParam: undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });;

使用

const { articles, isLoading, error, loadMoreProps } = useArticleList({
    queryKey: "articles",
    queryFn: (params) =>
      articleService.getArticles(params).then((res) => res.data),
 });